Browse Source

SONAR-10551 Import issues from external rule engines from generic report

tags/7.5
Duarte Meneses 6 years ago
parent
commit
9c04ec39f9
21 changed files with 917 additions and 5 deletions
  1. 6
    0
      sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java
  2. 1
    1
      sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java
  3. 5
    0
      sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java
  4. 124
    0
      sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueImporter.java
  5. 82
    0
      sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssuesImportSensor.java
  6. 133
    0
      sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ReportParser.java
  7. 4
    1
      sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java
  8. 1
    1
      sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/noop/NoOpNewExternalIssue.java
  9. 124
    0
      sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ReportParserTest.java
  10. 130
    0
      sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/ExternalIssuesMediumTest.java
  11. 1
    1
      sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java
  12. 58
    0
      sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/externalIssues.json
  13. 34
    0
      sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report.json
  14. 33
    0
      sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_invalid_json.json
  15. 31
    0
      sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_engineId.json
  16. 28
    0
      sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_filePath.json
  17. 25
    0
      sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_primaryLocation.json
  18. 32
    0
      sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_ruleId.json
  19. 32
    0
      sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_severity.json
  20. 32
    0
      sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_type.json
  21. 1
    1
      tests/src/test/java/org/sonarqube/tests/issue/ExternalIssueTest.java

+ 6
- 0
sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java View File

@@ -74,6 +74,12 @@ public interface CoreProperties {
*/
String SUBCATEGORY_L10N = "localization";

/**
* @since 7.2
*/
String CATEGORY_EXTERNAL_ISSUES = "externalIssues";

/**
* @since 2.11
*/

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

@@ -52,7 +52,7 @@ public class DefaultExternalIssue extends AbstractDefaultIssue<DefaultExternalIs
}

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

+ 5
- 0
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/BatchComponents.java View File

@@ -26,6 +26,7 @@ import org.sonar.core.component.DefaultResourceTypes;
import org.sonar.core.config.CorePropertyDefinitions;
import org.sonar.core.issue.tracking.Tracker;
import org.sonar.scanner.cpd.CpdComponents;
import org.sonar.scanner.externalissue.ExternalIssuesImportSensor;
import org.sonar.scanner.genericcoverage.GenericCoverageSensor;
import org.sonar.scanner.genericcoverage.GenericTestExecutionSensor;
import org.sonar.scanner.issue.tracking.ServerIssueFromWs;
@@ -70,6 +71,10 @@ public class BatchComponents {
components.add(GenericTestExecutionSensor.class);
components.addAll(GenericTestExecutionSensor.properties());

// External issues
components.add(ExternalIssuesImportSensor.class);
components.add(ExternalIssuesImportSensor.properties());

} else {
// Issues tracking
components.add(new Tracker<TrackedIssue, ServerIssueFromWs>());

+ 124
- 0
sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueImporter.java View File

@@ -0,0 +1,124 @@
/*
* 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.scanner.externalissue;

import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.fs.TextPointer;
import org.sonar.api.batch.rule.Severity;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.issue.NewExternalIssue;
import org.sonar.api.batch.sensor.issue.NewIssueLocation;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.scanner.externalissue.ReportParser.Issue;
import org.sonar.scanner.externalissue.ReportParser.Location;
import org.sonar.scanner.externalissue.ReportParser.Report;

public class ExternalIssueImporter {
private static final Logger LOG = Loggers.get(ExternalIssuesImportSensor.class);
private static final int MAX_UNKNOWN_FILE_PATHS_TO_PRINT = 5;

private final SensorContext context;
private final Report report;
private final Set<String> unknownFiles = new LinkedHashSet<>();
private final Set<String> knownFiles = new LinkedHashSet<>();

public ExternalIssueImporter(SensorContext context, Report report) {
this.context = context;
this.report = report;
}

public void execute() {
int issueCount = 0;

for (Issue issue : report.issues) {
NewExternalIssue externalIssue = context.newExternalIssue()
.forRule(RuleKey.of(issue.engineId, issue.ruleId))
.severity(Severity.valueOf(issue.severity))
.type(RuleType.valueOf(issue.type));

NewIssueLocation primary = fillLocation(context, externalIssue.newLocation(), issue.primaryLocation);
if (primary != null) {
knownFiles.add(issue.primaryLocation.filePath);
externalIssue.at(primary);
if (issue.secondaryLocations != null) {
for (Location l : issue.secondaryLocations) {
NewIssueLocation secondary = fillLocation(context, externalIssue.newLocation(), l);
if (secondary != null) {
externalIssue.addLocation(secondary);
}
}
}
issueCount++;
externalIssue.save();
} else {
unknownFiles.add(issue.primaryLocation.filePath);
}
}

LOG.info("Imported {} {} in {} {}", issueCount, pluralize("issue", issueCount), knownFiles.size(), pluralize("file", knownFiles.size()));
int numberOfUnknownFiles = unknownFiles.size();
if (numberOfUnknownFiles > 0) {
LOG.info("External issues ignored for " + numberOfUnknownFiles + " unknown files, including: "
+ unknownFiles.stream().limit(MAX_UNKNOWN_FILE_PATHS_TO_PRINT).collect(Collectors.joining(", ")));
}
}

private static String pluralize(String msg, int count) {
if (count == 1) {
return msg;
}
return msg + "s";
}

@CheckForNull
private NewIssueLocation fillLocation(SensorContext context, NewIssueLocation newLocation, Location location) {
InputFile file = findFile(context, location.filePath);
if (file != null) {
newLocation
.message(location.message)
.on(file);

if (location.textRange != null) {
if (location.textRange.startColumn != null) {
TextPointer start = file.newPointer(location.textRange.startLine, location.textRange.startColumn);
TextPointer end = file.newPointer(location.textRange.endLine, location.textRange.endColumn);
newLocation.at(file.newRange(start, end));
} else {
newLocation.at(file.selectLine(location.textRange.startLine));
}
}
return newLocation;
}
return null;
}

@CheckForNull
private InputFile findFile(SensorContext context, String filePath) {
return context.fileSystem().inputFile(context.fileSystem().predicates().hasPath(filePath));
}

}

+ 82
- 0
sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssuesImportSensor.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.scanner.externalissue;

import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.sonar.api.CoreProperties;
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.config.Configuration;
import org.sonar.api.config.PropertyDefinition;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.scanner.externalissue.ReportParser.Report;

public class ExternalIssuesImportSensor implements Sensor {
private static final Logger LOG = Loggers.get(ExternalIssuesImportSensor.class);
static final String REPORT_PATHS_PROPERTY_KEY = "sonar.externalIssuesReportPaths";

private final Configuration config;

public ExternalIssuesImportSensor(Configuration config) {
this.config = config;
}

public static List<PropertyDefinition> properties() {
return Collections.singletonList(
PropertyDefinition.builder(REPORT_PATHS_PROPERTY_KEY)
.name("Issues report paths")
.description("List of comma-separated paths (absolute or relative) containing report with issues created by external rule engines.")
.category(CoreProperties.CATEGORY_EXTERNAL_ISSUES)
.onQualifiers(Qualifiers.PROJECT)
.build());
}

@Override
public void describe(SensorDescriptor descriptor) {
descriptor.name("Import external issues report")
.onlyWhenConfiguration(c -> c.hasKey(REPORT_PATHS_PROPERTY_KEY));
}

@Override
public void execute(SensorContext context) {
Set<String> reportPaths = loadReportPaths();
for (String reportPath : reportPaths) {
LOG.debug("Importing issues from '{}'", reportPath);
Path reportFilePath = context.fileSystem().resolvePath(reportPath).toPath();
ReportParser parser = new ReportParser(reportFilePath);
Report report = parser.parse();
ExternalIssueImporter issueImporter = new ExternalIssueImporter(context, report);
issueImporter.execute();
}
}

private Set<String> loadReportPaths() {
return Arrays.stream(config.getStringArray(REPORT_PATHS_PROPERTY_KEY)).collect(Collectors.toSet());
}

}

+ 133
- 0
sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ReportParser.java View File

@@ -0,0 +1,133 @@
/*
* 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.scanner.externalissue;

import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import com.google.gson.JsonSyntaxException;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.annotation.Nullable;
import org.apache.commons.lang.StringUtils;

public class ReportParser {
private Gson gson = new Gson();
private Path filePath;

public ReportParser(Path filePath) {
this.filePath = filePath;
}

public Report parse() {
try (Reader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) {
return validate(gson.fromJson(reader, Report.class));
} catch (JsonIOException | IOException e) {
throw new IllegalStateException("Failed to read external issues report '" + filePath + "'", e);
} catch (JsonSyntaxException e) {
throw new IllegalStateException("Failed to read external issues report '" + filePath + "': invalid JSON syntax", e);
}
}

private Report validate(Report report) {
for (Issue issue : report.issues) {
mandatoryField(issue.primaryLocation, "primaryLocation");
mandatoryField(issue.engineId, "engineId");
mandatoryField(issue.ruleId, "ruleId");
mandatoryField(issue.severity, "severity");
mandatoryField(issue.type, "type");
mandatoryField(issue.primaryLocation, "primaryLocation");
mandatoryFieldPrimaryLocation(issue.primaryLocation.filePath, "filePath");
mandatoryFieldPrimaryLocation(issue.primaryLocation.message, "message");

if (issue.primaryLocation.textRange != null) {
mandatoryFieldPrimaryLocation(issue.primaryLocation.textRange.startLine, "startLine of the text range");
}
if (issue.secondaryLocations != null) {
for (Location l : issue.secondaryLocations) {
mandatoryFieldSecondaryLocation(l.filePath, "filePath");
mandatoryFieldSecondaryLocation(l.textRange, "textRange");
mandatoryFieldSecondaryLocation(l.textRange.startLine, "startLine of the text range");
}
}
}

return report;
}

private void mandatoryFieldPrimaryLocation(@Nullable Object value, String fieldName) {
if (value == null) {
throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s' in the primary location of the issue.", filePath, fieldName));
}
}
private void mandatoryFieldSecondaryLocation(@Nullable Object value, String fieldName) {
if (value == null) {
throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s' in a secondary location of the issue.", filePath, fieldName));
}
}

private void mandatoryField(@Nullable Object value, String fieldName) {
if (value == null) {
throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s'.", filePath, fieldName));
}
}

private void mandatoryField(@Nullable String value, String fieldName) {
if (StringUtils.isBlank(value)) {
throw new IllegalStateException(String.format("Failed to parse report '%s': missing mandatory field '%s'.", filePath, fieldName));
}
}

static class Report {
Issue[] issues;
}

static class Issue {
String engineId;
String ruleId;
String severity;
String type;
Location primaryLocation;
@Nullable
Location[] secondaryLocations;
}

static class Location {
@Nullable
String message;
String filePath;
@Nullable
TextRange textRange;
}

static class TextRange {
Integer startLine;
@Nullable
Integer startColumn;
@Nullable
Integer endLine;
@Nullable
Integer endColumn;
}
}

+ 4
- 1
sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java View File

@@ -37,6 +37,7 @@ import org.sonar.api.utils.MessageException;
import org.sonar.scanner.protocol.Constants.Severity;
import org.sonar.scanner.protocol.output.ScannerReport;
import org.sonar.scanner.protocol.output.ScannerReport.IssueLocation;
import org.sonar.scanner.protocol.output.ScannerReport.IssueType;
import org.sonar.scanner.report.ReportPublisher;

/**
@@ -113,12 +114,14 @@ public class ModuleIssues {
private static ScannerReport.ExternalIssue createReportExternalIssue(ExternalIssue issue, int componentRef) {
String primaryMessage = Strings.isNullOrEmpty(issue.primaryLocation().message()) ? issue.ruleKey().toString() : issue.primaryLocation().message();
Severity severity = Severity.valueOf(issue.severity().name());

IssueType issueType = IssueType.valueOf(issue.type().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.setType(issueType);
builder.setRuleRepository(issue.ruleKey().repository());
builder.setRuleKey(issue.ruleKey().rule());
builder.setMsg(primaryMessage);

+ 1
- 1
sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/noop/NoOpNewExternalIssue.java View File

@@ -41,7 +41,7 @@ public class NoOpNewExternalIssue implements NewExternalIssue {
}

@Override
public NewExternalIssue remediationEffort(Long effort) {
public NewExternalIssue remediationEffortMinutes(Long effort) {
// no op
return this;
}

+ 124
- 0
sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ReportParserTest.java View File

@@ -0,0 +1,124 @@
/*
* 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.scanner.externalissue;

import java.nio.file.Path;
import java.nio.file.Paths;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.scanner.externalissue.ReportParser.Report;

import static org.assertj.core.api.Assertions.assertThat;

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

@Test
public void parse_sample() {
ReportParser parser = new ReportParser(Paths.get("src/test/resources/org/sonar/scanner/externalissue/report.json"));

System.out.println(Paths.get("org/sonar/scanner/externalissue/report.json").toAbsolutePath());
Report report = parser.parse();

assertThat(report.issues).hasSize(2);
assertThat(report.issues[0].engineId).isEqualTo("eslint");
assertThat(report.issues[0].ruleId).isEqualTo("rule1");
assertThat(report.issues[0].severity).isEqualTo("MAJOR");
assertThat(report.issues[0].effortMinutes).isEqualTo(20);
assertThat(report.issues[0].type).isEqualTo("CODE_SMELL");
assertThat(report.issues[0].primaryLocation.filePath).isEqualTo("file1.js");
assertThat(report.issues[0].primaryLocation.message).isEqualTo("fix the issue here");
assertThat(report.issues[0].primaryLocation.textRange.startColumn).isNull();
assertThat(report.issues[0].primaryLocation.textRange.startLine).isEqualTo(1);
assertThat(report.issues[0].primaryLocation.textRange.endColumn).isNull();
assertThat(report.issues[0].primaryLocation.textRange.endLine).isEqualTo(2);
assertThat(report.issues[0].secondaryLocations).isNull();
}

private Path path(String reportName) {
return Paths.get("src/test/resources/org/sonar/scanner/externalissue/" + reportName);
}

@Test
public void fail_if_report_doesnt_exist() {
ReportParser parser = new ReportParser(Paths.get("unknown.json"));
exception.expect(IllegalStateException.class);
exception.expectMessage("Failed to read external issues report 'unknown.json'");
parser.parse();
}

@Test
public void fail_if_report_is_not_valid_json() {
ReportParser parser = new ReportParser(path("report_invalid_json.json"));
exception.expect(IllegalStateException.class);
exception.expectMessage("invalid JSON syntax");
parser.parse();
}

@Test
public void fail_if_primaryLocation_not_set() {
ReportParser parser = new ReportParser(path("report_missing_primaryLocation.json"));
exception.expect(IllegalStateException.class);
exception.expectMessage("missing mandatory field 'primaryLocation'");
parser.parse();
}

@Test
public void fail_if_engineId_not_set() {
ReportParser parser = new ReportParser(path("report_missing_engineId.json"));
exception.expect(IllegalStateException.class);
exception.expectMessage("missing mandatory field 'engineId'");
parser.parse();
}

@Test
public void fail_if_ruleId_not_set() {
ReportParser parser = new ReportParser(path("report_missing_ruleId.json"));
exception.expect(IllegalStateException.class);
exception.expectMessage("missing mandatory field 'ruleId'");
parser.parse();
}

@Test
public void fail_if_severity_not_set() {
ReportParser parser = new ReportParser(path("report_missing_severity.json"));
exception.expect(IllegalStateException.class);
exception.expectMessage("missing mandatory field 'severity'");
parser.parse();
}

@Test
public void fail_if_type_not_set() {
ReportParser parser = new ReportParser(path("report_missing_type.json"));
exception.expect(IllegalStateException.class);
exception.expectMessage("missing mandatory field 'type'");
parser.parse();
}

@Test
public void fail_if_filePath_not_set_in_primaryLocation() {
ReportParser parser = new ReportParser(path("report_missing_filePath.json"));
exception.expect(IllegalStateException.class);
exception.expectMessage("missing mandatory field 'filePath'");
parser.parse();
}
}

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

@@ -0,0 +1,130 @@
/*
* 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.scanner.mediumtest.issues;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.sonar.api.utils.log.LogTester;
import org.sonar.api.utils.log.LoggersTest;
import org.sonar.scanner.mediumtest.ScannerMediumTester;
import org.sonar.scanner.mediumtest.TaskResult;
import org.sonar.scanner.protocol.Constants.Severity;
import org.sonar.scanner.protocol.output.ScannerReport.ExternalIssue;
import org.sonar.scanner.protocol.output.ScannerReport.Issue;
import org.sonar.scanner.protocol.output.ScannerReport.IssueType;
import org.sonar.xoo.XooPlugin;

import static org.assertj.core.api.Assertions.assertThat;

public class ExternalIssuesMediumTest {
@Rule
public LogTester logs = new LogTester();
@Rule
public TemporaryFolder temp = new TemporaryFolder();

@Rule
public ScannerMediumTester tester = new ScannerMediumTester()
.registerPlugin("xoo", new XooPlugin());

@Test
public void testOneIssuePerLine() 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("sonar.oneExternalIssuePerLine.activate", "true")
.execute();

List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
assertThat(issues).isEmpty();

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

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

@Test
public void testLoadIssuesFromJsonReport() throws URISyntaxException, IOException {
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("sonar.externalIssuesReportPaths", "externalIssues.json")
.execute();

List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo"));
assertThat(issues).isEmpty();

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

// precise issue location
ExternalIssue issue = externalIssues.get(0);
assertThat(issue.getFlowCount()).isZero();
assertThat(issue.getMsg()).isEqualTo("fix the issue here");
assertThat(issue.getRuleKey()).isEqualTo("rule1");
assertThat(issue.getSeverity()).isEqualTo(Severity.MAJOR);
assertThat(issue.getType()).isEqualTo(IssueType.CODE_SMELL);
assertThat(issue.getTextRange().getStartLine()).isEqualTo(5);
assertThat(issue.getTextRange().getEndLine()).isEqualTo(5);
assertThat(issue.getTextRange().getStartOffset()).isEqualTo(3);
assertThat(issue.getTextRange().getEndOffset()).isEqualTo(41);

// location on a line
issue = externalIssues.get(1);
assertThat(issue.getFlowCount()).isZero();
assertThat(issue.getMsg()).isEqualTo("fix the bug here");
assertThat(issue.getRuleKey()).isEqualTo("rule2");
assertThat(issue.getSeverity()).isEqualTo(Severity.CRITICAL);
assertThat(issue.getType()).isEqualTo(IssueType.BUG);
assertThat(issue.getTextRange().getStartLine()).isEqualTo(3);
assertThat(issue.getTextRange().getEndLine()).isEqualTo(3);
assertThat(issue.getTextRange().getStartOffset()).isEqualTo(0);
assertThat(issue.getTextRange().getEndOffset()).isEqualTo(24);

// One file-level issue in helloscala
List<ExternalIssue> externalIssues2 = result.externalIssuesFor(result.inputFile("xources/hello/helloscala.xoo"));
assertThat(externalIssues2).hasSize(1);

issue = externalIssues2.iterator().next();
assertThat(issue.getFlowCount()).isZero();
assertThat(issue.getMsg()).isEqualTo("fix the bug here");
assertThat(issue.getRuleKey()).isEqualTo("rule3");
assertThat(issue.getSeverity()).isEqualTo(Severity.MAJOR);
assertThat(issue.getType()).isEqualTo(IssueType.BUG);
assertThat(issue.hasTextRange()).isFalse();
// one issue is located in a non-existing file
assertThat(logs.logs()).contains("External issues ignored for 1 unknown files, including: invalidFile");

}
}

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

@@ -40,7 +40,7 @@ import static org.assertj.core.api.Assertions.tuple;

public class IssuesMediumTest {

@org.junit.Rule
@Rule
public TemporaryFolder temp = new TemporaryFolder();

@Rule

+ 58
- 0
sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/externalIssues.json View File

@@ -0,0 +1,58 @@
{
"issues" : [
{
"engineId": "externalXoo",
"ruleId": "rule1",
"severity": "MAJOR",
"type": "CODE_SMELL",
"primaryLocation": {
"message": "fix the issue here",
"filePath": "xources/hello/HelloJava.xoo",
"textRange": {
"startLine": 5,
"startColumn": 3,
"endLine": 5,
"endColumn": 41
}
}
},
{
"engineId": "externalXoo",
"ruleId": "rule2",
"severity": "CRITICAL",
"type": "BUG",
"primaryLocation": {
"message": "fix the bug here",
"filePath": "xources/hello/HelloJava.xoo",
"textRange": {
"startLine": 3,
"endLine": 3
}
}
},
{
"engineId": "externalXoo",
"ruleId": "rule2",
"severity": "CRITICAL",
"type": "BUG",
"primaryLocation": {
"message": "fix the bug here",
"filePath": "invalidFile",
"textRange": {
"startLine": 3,
"endLine": 3
}
}
},
{
"engineId": "externalXoo",
"ruleId": "rule3",
"severity": "MAJOR",
"type": "BUG",
"primaryLocation": {
"message": "fix the bug here",
"filePath": "xources/hello/helloscala.xoo"
}
}
]
}

+ 34
- 0
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report.json View File

@@ -0,0 +1,34 @@
{
"issues" : [
{
"engineId": "eslint",
"ruleId": "rule1",
"severity": "MAJOR",
"type": "CODE_SMELL",
"effortMinutes": 20,
"primaryLocation": {
"message": "fix the issue here",
"filePath": "file1.js",
"textRange": {
"startLine": 1,
"endLine": 2
}
}
},
{
"engineId": "eslint",
"ruleId": "rule2",
"severity": "MAJOR",
"type": "BUG",
"primaryLocation": {
"message": "fix the bug here",
"filePath": "file2.js",
"textRange": {
"startLine": 3,
"endLine": 4
}
}
}
]
}

+ 33
- 0
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_invalid_json.json View File

@@ -0,0 +1,33 @@
{
"issues" : [
{
"engineId": "eslint",
"ruleId": "rule1",
"severity": "MAJOR",
"type": "CODE_SMELL",
"primaryLocation": {
"message": "fix the issue here",
"filePath": "file1.js",
"textRange": {
"startLine": 1,
"endLine": 2,
}
}
},
{
"engineId": "eslint",
"ruleId": "rule2",
"severity": "MAJOR",
"type": "BUG",
"primaryLocation": {
"message": "fix the bug here",
"filePath": "file2.js",
"textRange": {
"startLine": 3,
"endLine": 4
}
}
}
]
}

+ 31
- 0
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_engineId.json View File

@@ -0,0 +1,31 @@
{
"issues" : [
{
"engineId": "eslint",
"ruleId": "rule1",
"severity": "MAJOR",
"type": "CODE_SMELL",
"primaryLocation": {
"message": "fix the issue here",
"filePath": "file1.js",
"textRange": {
"startLine": 1,
"endLine": 2
}
}
},
{
"ruleId": "rule2",
"severity": "MAJOR",
"type": "BUG",
"primaryLocation": {
"message": "fix the bug here",
"filePath": "file2.js",
"textRange": {
"startLine": 3
}
}
}
]
}

+ 28
- 0
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_filePath.json View File

@@ -0,0 +1,28 @@
{
"issues" : [
{
"engineId": "eslint",
"ruleId": "rule1",
"severity": "MAJOR",
"type": "CODE_SMELL",
"primaryLocation": {
"message": "fix the issue here",
"filePath": "file1.js",
"textRange": {
"startLine": 1,
"endLine": 2
}
}
},
{
"engineId": "eslint",
"ruleId": "rule2",
"severity": "MAJOR",
"type": "BUG",
"primaryLocation": {
"message": "fix the bug here"
}
}
]
}

+ 25
- 0
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_primaryLocation.json View File

@@ -0,0 +1,25 @@
{
"issues" : [
{
"engineId": "eslint",
"ruleId": "rule1",
"severity": "MAJOR",
"type": "CODE_SMELL",
"primaryLocation": {
"message": "fix the issue here",
"filePath": "file1.js",
"textRange": {
"startLine": 1,
"endLine": 2
}
}
},
{
"engineId": "eslint",
"ruleId": "rule2",
"severity": "MAJOR",
"type": "BUG"
}
]
}

+ 32
- 0
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_ruleId.json View File

@@ -0,0 +1,32 @@
{
"issues" : [
{
"engineId": "eslint",
"ruleId": "rule1",
"severity": "MAJOR",
"type": "CODE_SMELL",
"primaryLocation": {
"message": "fix the issue here",
"filePath": "file1.js",
"textRange": {
"startLine": 1,
"endLine": 2
}
}
},
{
"engineId": "eslint",
"severity": "MAJOR",
"type": "BUG",
"primaryLocation": {
"message": "fix the bug here",
"filePath": "file2.js",
"textRange": {
"startLine": 3,
"endLine": 4
}
}
}
]
}

+ 32
- 0
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_severity.json View File

@@ -0,0 +1,32 @@
{
"issues" : [
{
"engineId": "eslint",
"ruleId": "rule1",
"severity": "MAJOR",
"type": "CODE_SMELL",
"primaryLocation": {
"message": "fix the issue here",
"filePath": "file1.js",
"textRange": {
"startLine": 1,
"endLine": 2
}
}
},
{
"engineId": "eslint",
"ruleId": "rule2",
"type": "BUG",
"primaryLocation": {
"message": "fix the bug here",
"filePath": "file2.js",
"textRange": {
"startLine": 3,
"endLine": 4
}
}
}
]
}

+ 32
- 0
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_type.json View File

@@ -0,0 +1,32 @@
{
"issues" : [
{
"engineId": "eslint",
"ruleId": "rule1",
"severity": "MAJOR",
"type": "CODE_SMELL",
"primaryLocation": {
"message": "fix the issue here",
"filePath": "file1.js",
"textRange": {
"startLine": 1,
"endLine": 2
}
}
},
{
"engineId": "eslint",
"ruleId": "rule2",
"severity": "MAJOR",
"primaryLocation": {
"message": "fix the bug here",
"filePath": "file2.js",
"textRange": {
"startLine": 3,
"endLine": 4
}
}
}
]
}

+ 1
- 1
tests/src/test/java/org/sonarqube/tests/issue/ExternalIssueTest.java View File

@@ -64,7 +64,7 @@ public class ExternalIssueTest extends AbstractIssueTest {
assertThat(issuesList).allMatch(issue -> RuleType.CODE_SMELL.equals(issue.getType()));
assertThat(issuesList).allMatch(issue -> "sample:src/main/xoo/sample/Sample.xoo".equals(issue.getComponent()));
assertThat(issuesList).allMatch(issue -> "OPEN".equals(issue.getStatus()));
assertThat(issuesList).allMatch(issue -> Boolean.TRUE.equals(issue.getFromExternalRule()));
assertThat(issuesList).allMatch(issue -> issue.getExternalRuleEngine().equals("xoo"));

List<org.sonarqube.ws.Rules.Rule> rulesList = tester.wsClient().rules()
.search(new org.sonarqube.ws.client.rules.SearchRequest().setIsExternal(Boolean.toString(true))).getRulesList();

Loading…
Cancel
Save