@@ -74,6 +74,12 @@ public interface CoreProperties { | |||
*/ | |||
String SUBCATEGORY_L10N = "localization"; | |||
/** | |||
* @since 7.2 | |||
*/ | |||
String CATEGORY_EXTERNAL_ISSUES = "externalIssues"; | |||
/** | |||
* @since 2.11 | |||
*/ |
@@ -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; | |||
} |
@@ -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>()); |
@@ -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)); | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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); |
@@ -41,7 +41,7 @@ public class NoOpNewExternalIssue implements NewExternalIssue { | |||
} | |||
@Override | |||
public NewExternalIssue remediationEffort(Long effort) { | |||
public NewExternalIssue remediationEffortMinutes(Long effort) { | |||
// no op | |||
return this; | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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"); | |||
} | |||
} |
@@ -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 |
@@ -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" | |||
} | |||
} | |||
] | |||
} |
@@ -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 | |||
} | |||
} | |||
} | |||
] | |||
} | |||
@@ -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 | |||
} | |||
} | |||
} | |||
] | |||
} | |||
@@ -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 | |||
} | |||
} | |||
} | |||
] | |||
} | |||
@@ -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" | |||
} | |||
} | |||
] | |||
} | |||
@@ -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" | |||
} | |||
] | |||
} | |||
@@ -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 | |||
} | |||
} | |||
} | |||
] | |||
} | |||
@@ -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 | |||
} | |||
} | |||
} | |||
] | |||
} | |||
@@ -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 | |||
} | |||
} | |||
} | |||
] | |||
} | |||
@@ -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(); |