From: Duarte Meneses Date: Fri, 6 Apr 2018 09:33:23 +0000 (+0200) Subject: SONAR-10543 Sensor Java API should allow to add external rule engine issues X-Git-Tag: 7.5~1329 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=7bd31bc52d296c558803ee633c49851afe13e9ff;p=sonarqube.git SONAR-10543 Sensor Java API should allow to add external rule engine issues --- diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java index 97e59ef3156..3c036b1d566 100644 --- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java @@ -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); + } } } diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssuePerLineSensor.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssuePerLineSensor.java new file mode 100644 index 00000000000..4dbf744243b --- /dev/null +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneExternalIssuePerLineSensor.java @@ -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(); + } + } +} diff --git a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java index 70f8d253b46..6384ffa58c6 100644 --- a/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java +++ b/plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/XooPluginTest.java @@ -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); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java index 2ae66410abc..9557e969e2c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java @@ -39,6 +39,8 @@ public interface BatchReportReader { ScannerReport.Component readComponent(int componentRef); CloseableIterator readComponentIssues(int componentRef); + + CloseableIterator readComponentExternalIssues(int componentRef); CloseableIterator readComponentDuplications(int componentRef); diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java index 9ef182d94fa..b7dc446fe28 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java @@ -108,6 +108,12 @@ public class BatchReportReaderImpl implements BatchReportReader { ensureInitialized(); return delegate.readComponentIssues(componentRef); } + + @Override + public CloseableIterator readComponentExternalIssues(int componentRef) { + ensureInitialized(); + return delegate.readComponentExternalIssues(componentRef); + } @Override public CloseableIterator readComponentDuplications(int componentRef) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackerRawInputFactory.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackerRawInputFactory.java index b92d04c2a99..bd2e6f6d396 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackerRawInputFactory.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackerRawInputFactory.java @@ -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 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); diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java index caaf43ada0e..cfeaa8ab89c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java @@ -44,6 +44,7 @@ public class BatchReportReaderRule implements TestRule, BatchReportReader { private Map changesets = new HashMap<>(); private Map components = new HashMap<>(); private Map> issues = new HashMap<>(); + private Map> externalIssues = new HashMap<>(); private Map> duplications = new HashMap<>(); private Map> duplicationBlocks = new HashMap<>(); private Map> symbols = new HashMap<>(); @@ -172,6 +173,11 @@ public class BatchReportReaderRule implements TestRule, BatchReportReader { public CloseableIterator readComponentIssues(int componentRef) { return closeableIterator(issues.get(componentRef)); } + + @Override + public CloseableIterator readComponentExternalIssues(int componentRef) { + return closeableIterator(externalIssues.get(componentRef)); + } public BatchReportReaderRule putIssues(int componentRef, List issue) { this.issues.put(componentRef, issue); diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java index e1a6f6cdf57..287fc2f89b3 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java @@ -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 ------------ /** diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java index b8e8c969160..ef225532139 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java @@ -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 measuresByComponentAndMetric = HashBasedTable.create(); Collection allIssues = new ArrayList<>(); + Collection allExternalIssues = new ArrayList<>(); Collection allAnalysisErrors = new ArrayList<>(); Map 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); + } } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java index b6968768d17..b13340fb1c1 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java @@ -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 allExternalIssues() { + return sensorStorage.allExternalIssues; + } + public Collection allAnalysisErrors() { return sensorStorage.allAnalysisErrors; } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java index d2dbcb416da..d8c7cb5648f 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java @@ -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); /** diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/ExternalIssue.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/ExternalIssue.java new file mode 100644 index 00000000000..cdc7d6df2f2 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/ExternalIssue.java @@ -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(); +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/IIssue.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/IIssue.java new file mode 100644 index 00000000000..32ac7acf9e2 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/IIssue.java @@ -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 flows(); +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/Issue.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/Issue.java index 5bb21addabb..0f752778775 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/Issue.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/Issue.java @@ -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 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 flows(); - } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/NewExternalIssue.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/NewExternalIssue.java new file mode 100644 index 00000000000..2a3f41b4f60 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/NewExternalIssue.java @@ -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 path that backtracks the issue from its primary location to the start of the flow. + * Several flows can be registered. + */ + NewExternalIssue addFlow(Iterable 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(); + +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java new file mode 100644 index 00000000000..694942984c2 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java @@ -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 extends DefaultStorable { + protected RuleKey ruleKey; + protected IssueLocation primaryLocation; + protected List> 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 flows() { + return this.flows.stream() + .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 locations) { + List flowAsList = new ArrayList<>(); + for (NewIssueLocation issueLocation : locations) { + flowAsList.add((DefaultIssueLocation) issueLocation); + } + flows.add(flowAsList); + return (T) this; + } + +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java new file mode 100644 index 00000000000..de3eed8b4e4 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssue.java @@ -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 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; + } +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java index 8b1f8bcb2f0..30095731169 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java @@ -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 implements Issue, NewIssue { private Double gap; private Severity overriddenSeverity; - private IssueLocation primaryLocation; - private List> 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 locations) { - List 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 flows() { - return this.flows.stream() - .map(l -> () -> unmodifiableList(new ArrayList<>(l))) - .collect(toList()); - } - @Override public void doSave() { requireNonNull(this.ruleKey, "ruleKey is mandatory on issue"); diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleKey.java b/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleKey.java index 25378ef02e6..d206d512fbd 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleKey.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/rule/RuleKey.java @@ -39,6 +39,7 @@ public class RuleKey implements Serializable, Comparable { */ @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; diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java index a8f57a13ef8..0cee9bc2a96 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java @@ -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(); diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssueTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssueTest.java new file mode 100644 index 00000000000..d219cefc8ed --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultExternalIssueTest.java @@ -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(); + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java index ef7bda53f16..40167c3c769 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/ModuleIssues.java @@ -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 consumer, ScannerReport.IssueLocation.Builder locationBuilder, + ScannerReport.TextRange.Builder textRangeBuilder, Collection 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); + } } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/TaskResult.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/TaskResult.java index 232fbc88c06..80a33078927 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/TaskResult.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/mediumtest/TaskResult.java @@ -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 externalIssuesFor(InputComponent inputComponent) { + int ref = reportComponents.get(inputComponent.key()).getRef(); + return externalIssuesFor(ref); + } public List issuesFor(Component reportComponent) { int ref = reportComponent.getRef(); @@ -138,6 +143,16 @@ public class TaskResult implements org.sonar.scanner.mediumtest.ScanTaskObserver } return result; } + + private List externalIssuesFor(int ref) { + List result = Lists.newArrayList(); + try (CloseableIterator it = reader.readComponentExternalIssues(ref)) { + while (it.hasNext()) { + result.add(it.next()); + } + } + return result; + } public Collection inputFiles() { return inputFiles.values(); diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java index 6972e241500..ff9a7ba1685 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java @@ -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); } + } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java index 6b5a7137d94..1ccd219c600 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java @@ -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); } + } diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/ModuleIssuesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/ModuleIssuesTest.java index 07f627c8423..6309720339d 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/ModuleIssuesTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/ModuleIssuesTest.java @@ -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 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); diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java index 64c8b66cb84..babe6f078ba 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/IssuesMediumTest.java @@ -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 issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo")); assertThat(issues).hasSize(8 /* lines */); + + List 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 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 { diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorContextTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorContextTest.java index 68771fe5059..24cc70506be 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorContextTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorContextTest.java @@ -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(); diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java index 843e4730b45..bb039e2da18 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java @@ -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 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(); diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java index bf750bd0722..019e64d169d 100644 --- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java +++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java @@ -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), diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java index c6e3c94fa84..2fb837b2558 100644 --- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java +++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java @@ -83,6 +83,14 @@ public class ScannerReportReader { return emptyCloseableIterator(); } + public CloseableIterator 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 readComponentDuplications(int componentRef) { File file = fileStructure.fileFor(FileStructure.Domain.DUPLICATIONS, componentRef); if (fileExists(file)) { diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java index 8b3b9c8202b..ebb6fe925e2 100644 --- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java +++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java @@ -82,6 +82,21 @@ public class ScannerReportWriter { } } + public File writeComponentExternalIssues(int componentRef, Iterable 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 measures) { File file = fileStructure.fileFor(FileStructure.Domain.MEASURES, componentRef); Protobuf.writeStream(measures, file, false); diff --git a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto index c99b9c9882e..9bdb00bd4cf 100644 --- a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto +++ b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto @@ -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. diff --git a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java index 07b8d46e1f5..9a46a5ba1d6 100644 --- a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java +++ b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java @@ -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 diff --git a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportReaderTest.java b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportReaderTest.java index 6430f8866e9..6791f83a5f4 100644 --- a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportReaderTest.java +++ b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportReaderTest.java @@ -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(); diff --git a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java index 647ed49a40e..f4ee4dc3ed3 100644 --- a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java +++ b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java @@ -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 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();