@@ -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); | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -39,6 +39,8 @@ public interface BatchReportReader { | |||
ScannerReport.Component readComponent(int componentRef); | |||
CloseableIterator<ScannerReport.Issue> readComponentIssues(int componentRef); | |||
CloseableIterator<ScannerReport.ExternalIssue> readComponentExternalIssues(int componentRef); | |||
CloseableIterator<ScannerReport.Duplication> readComponentDuplications(int componentRef); | |||
@@ -108,6 +108,12 @@ public class BatchReportReaderImpl implements BatchReportReader { | |||
ensureInitialized(); | |||
return delegate.readComponentIssues(componentRef); | |||
} | |||
@Override | |||
public CloseableIterator<ScannerReport.ExternalIssue> readComponentExternalIssues(int componentRef) { | |||
ensureInitialized(); | |||
return delegate.readComponentExternalIssues(componentRef); | |||
} | |||
@Override | |||
public CloseableIterator<ScannerReport.Duplication> readComponentDuplications(int componentRef) { |
@@ -22,8 +22,11 @@ package org.sonar.server.computation.task.projectanalysis.issue; | |||
import java.util.ArrayList; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.rules.RuleType; | |||
import org.sonar.api.utils.Duration; | |||
import org.sonar.api.utils.log.Loggers; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.core.issue.tracking.Input; | |||
@@ -34,6 +37,7 @@ import org.sonar.db.protobuf.DbCommons; | |||
import org.sonar.db.protobuf.DbIssues; | |||
import org.sonar.scanner.protocol.Constants.Severity; | |||
import org.sonar.scanner.protocol.output.ScannerReport; | |||
import org.sonar.scanner.protocol.output.ScannerReport.IssueType; | |||
import org.sonar.server.computation.task.projectanalysis.batch.BatchReportReader; | |||
import org.sonar.server.computation.task.projectanalysis.component.Component; | |||
import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolder; | |||
@@ -111,6 +115,16 @@ public class TrackerRawInputFactory { | |||
} | |||
} | |||
try (CloseableIterator<ScannerReport.ExternalIssue> reportIssues = reportReader.readComponentExternalIssues(component.getReportAttributes().getRef())) { | |||
// optimization - do not load line hashes if there are no issues -> getLineHashSequence() is executed | |||
// as late as possible | |||
while (reportIssues.hasNext()) { | |||
ScannerReport.ExternalIssue reportExternalIssue = reportIssues.next(); | |||
DefaultIssue issue = toIssue(getLineHashSequence(), reportExternalIssue); | |||
result.add(issue); | |||
} | |||
} | |||
return result; | |||
} | |||
@@ -157,6 +171,59 @@ public class TrackerRawInputFactory { | |||
return issue; | |||
} | |||
private DefaultIssue toIssue(LineHashSequence lineHashSeq, ScannerReport.ExternalIssue reportIssue) { | |||
DefaultIssue issue = new DefaultIssue(); | |||
init(issue); | |||
issue.setRuleKey(RuleKey.of(reportIssue.getRuleRepository(), reportIssue.getRuleKey())); | |||
if (reportIssue.hasTextRange()) { | |||
int startLine = reportIssue.getTextRange().getStartLine(); | |||
issue.setLine(startLine); | |||
issue.setChecksum(lineHashSeq.getHashForLine(startLine)); | |||
} else { | |||
issue.setChecksum(""); | |||
} | |||
if (isNotEmpty(reportIssue.getMsg())) { | |||
issue.setMessage(reportIssue.getMsg()); | |||
} | |||
if (reportIssue.getSeverity() != Severity.UNSET_SEVERITY) { | |||
issue.setSeverity(reportIssue.getSeverity().name()); | |||
} | |||
if (reportIssue.getEffort() != 0) { | |||
issue.setEffort(Duration.create(reportIssue.getEffort())); | |||
} | |||
DbIssues.Locations.Builder dbLocationsBuilder = DbIssues.Locations.newBuilder(); | |||
if (reportIssue.hasTextRange()) { | |||
dbLocationsBuilder.setTextRange(convertTextRange(reportIssue.getTextRange())); | |||
} | |||
for (ScannerReport.Flow flow : reportIssue.getFlowList()) { | |||
if (flow.getLocationCount() > 0) { | |||
DbIssues.Flow.Builder dbFlowBuilder = DbIssues.Flow.newBuilder(); | |||
for (ScannerReport.IssueLocation location : flow.getLocationList()) { | |||
dbFlowBuilder.addLocation(convertLocation(location)); | |||
} | |||
dbLocationsBuilder.addFlow(dbFlowBuilder); | |||
} | |||
} | |||
issue.setLocations(dbLocationsBuilder.build()); | |||
issue.setType(toRuleType(reportIssue.getType())); | |||
issue.setDescriptionUrl(StringUtils.stripToNull(reportIssue.getDescriptionUrl())); | |||
return issue; | |||
} | |||
private RuleType toRuleType(IssueType type) { | |||
switch (type) { | |||
case BUG: | |||
return RuleType.BUG; | |||
case CODE_SMELL: | |||
return RuleType.CODE_SMELL; | |||
case VULNERABILITY: | |||
return RuleType.VULNERABILITY; | |||
case UNRECOGNIZED: | |||
default: | |||
throw new IllegalStateException("Invalid issue type: " + type); | |||
} | |||
} | |||
private DefaultIssue init(DefaultIssue issue) { | |||
issue.setResolution(null); | |||
issue.setStatus(Issue.STATUS_OPEN); |
@@ -44,6 +44,7 @@ public class BatchReportReaderRule implements TestRule, BatchReportReader { | |||
private Map<Integer, ScannerReport.Changesets> changesets = new HashMap<>(); | |||
private Map<Integer, ScannerReport.Component> components = new HashMap<>(); | |||
private Map<Integer, List<ScannerReport.Issue>> issues = new HashMap<>(); | |||
private Map<Integer, List<ScannerReport.ExternalIssue>> externalIssues = new HashMap<>(); | |||
private Map<Integer, List<ScannerReport.Duplication>> duplications = new HashMap<>(); | |||
private Map<Integer, List<ScannerReport.CpdTextBlock>> duplicationBlocks = new HashMap<>(); | |||
private Map<Integer, List<ScannerReport.Symbol>> symbols = new HashMap<>(); | |||
@@ -172,6 +173,11 @@ public class BatchReportReaderRule implements TestRule, BatchReportReader { | |||
public CloseableIterator<ScannerReport.Issue> readComponentIssues(int componentRef) { | |||
return closeableIterator(issues.get(componentRef)); | |||
} | |||
@Override | |||
public CloseableIterator<ScannerReport.ExternalIssue> readComponentExternalIssues(int componentRef) { | |||
return closeableIterator(externalIssues.get(componentRef)); | |||
} | |||
public BatchReportReaderRule putIssues(int componentRef, List<ScannerReport.Issue> issue) { | |||
this.issues.put(componentRef, issue); |
@@ -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 ------------ | |||
/** |
@@ -31,6 +31,7 @@ import org.sonar.api.batch.sensor.coverage.internal.DefaultCoverage; | |||
import org.sonar.api.batch.sensor.cpd.internal.DefaultCpdTokens; | |||
import org.sonar.api.batch.sensor.error.AnalysisError; | |||
import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting; | |||
import org.sonar.api.batch.sensor.issue.ExternalIssue; | |||
import org.sonar.api.batch.sensor.issue.Issue; | |||
import org.sonar.api.batch.sensor.measure.Measure; | |||
import org.sonar.api.batch.sensor.symbol.internal.DefaultSymbolTable; | |||
@@ -43,6 +44,7 @@ class InMemorySensorStorage implements SensorStorage { | |||
Table<String, String, Measure> measuresByComponentAndMetric = HashBasedTable.create(); | |||
Collection<Issue> allIssues = new ArrayList<>(); | |||
Collection<ExternalIssue> allExternalIssues = new ArrayList<>(); | |||
Collection<AnalysisError> allAnalysisErrors = new ArrayList<>(); | |||
Map<String, DefaultHighlighting> highlightingByComponent = new HashMap<>(); | |||
@@ -114,4 +116,9 @@ class InMemorySensorStorage implements SensorStorage { | |||
checkArgument(value != null, "Value of context property must not be null"); | |||
contextProperties.put(key, value); | |||
} | |||
@Override | |||
public void store(ExternalIssue issue) { | |||
allExternalIssues.add(issue); | |||
} | |||
} |
@@ -58,8 +58,11 @@ import org.sonar.api.batch.sensor.highlighting.NewHighlighting; | |||
import org.sonar.api.batch.sensor.highlighting.TypeOfText; | |||
import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting; | |||
import org.sonar.api.batch.sensor.highlighting.internal.SyntaxHighlightingRule; | |||
import org.sonar.api.batch.sensor.issue.ExternalIssue; | |||
import org.sonar.api.batch.sensor.issue.Issue; | |||
import org.sonar.api.batch.sensor.issue.NewExternalIssue; | |||
import org.sonar.api.batch.sensor.issue.NewIssue; | |||
import org.sonar.api.batch.sensor.issue.internal.DefaultExternalIssue; | |||
import org.sonar.api.batch.sensor.issue.internal.DefaultIssue; | |||
import org.sonar.api.batch.sensor.measure.Measure; | |||
import org.sonar.api.batch.sensor.measure.NewMeasure; | |||
@@ -219,6 +222,15 @@ public class SensorContextTester implements SensorContext { | |||
return sensorStorage.allIssues; | |||
} | |||
@Override | |||
public NewExternalIssue newExternalIssue() { | |||
return new DefaultExternalIssue(sensorStorage); | |||
} | |||
public Collection<ExternalIssue> allExternalIssues() { | |||
return sensorStorage.allExternalIssues; | |||
} | |||
public Collection<AnalysisError> allAnalysisErrors() { | |||
return sensorStorage.allAnalysisErrors; | |||
} |
@@ -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); | |||
/** |
@@ -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(); | |||
} |
@@ -0,0 +1,44 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.api.batch.sensor.issue; | |||
import java.util.List; | |||
import org.sonar.api.batch.sensor.issue.Issue.Flow; | |||
import org.sonar.api.rule.RuleKey; | |||
/** | |||
* @since 7.2 | |||
*/ | |||
public interface IIssue { | |||
/** | |||
* The {@link RuleKey} of this issue. | |||
*/ | |||
RuleKey ruleKey(); | |||
/** | |||
* Primary locations for this issue. | |||
*/ | |||
IssueLocation primaryLocation(); | |||
/** | |||
* List of flows for this issue. Can be empty. | |||
*/ | |||
List<Flow> flows(); | |||
} |
@@ -23,27 +23,20 @@ import java.util.List; | |||
import javax.annotation.CheckForNull; | |||
import org.sonar.api.batch.rule.Severity; | |||
import org.sonar.api.batch.sensor.Sensor; | |||
import org.sonar.api.rule.RuleKey; | |||
/** | |||
* Represents an issue detected by a {@link Sensor}. | |||
* | |||
* @since 5.1 | |||
*/ | |||
public interface Issue { | |||
public interface Issue extends IIssue { | |||
interface Flow { | |||
/** | |||
* @return Ordered list of locations for the execution flow | |||
*/ | |||
List<IssueLocation> locations(); | |||
} | |||
/** | |||
* The {@link RuleKey} of this issue. | |||
*/ | |||
RuleKey ruleKey(); | |||
/** | |||
* Effort to fix the issue. Used by technical debt model. | |||
* @deprecated since 5.5 use {@link #gap()} | |||
@@ -58,23 +51,24 @@ public interface Issue { | |||
*/ | |||
@CheckForNull | |||
Double gap(); | |||
/** | |||
* Overridden severity. | |||
*/ | |||
@CheckForNull | |||
Severity overriddenSeverity(); | |||
/** | |||
* Primary locations for this issue. | |||
* @since 5.2 | |||
*/ | |||
@Override | |||
IssueLocation primaryLocation(); | |||
/** | |||
* List of flows for this issue. Can be empty. | |||
* @since 5.2 | |||
*/ | |||
@Override | |||
List<Flow> flows(); | |||
} |
@@ -0,0 +1,88 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.api.batch.sensor.issue; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.batch.rule.Severity; | |||
import org.sonar.api.batch.sensor.Sensor; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.rules.RuleType; | |||
/** | |||
* Builder for an issue imported from an external rule engine by a {@link Sensor}. | |||
* Don't forget to {@link #save()} after setting the fields. | |||
* | |||
* @since 7.2 | |||
*/ | |||
public interface NewExternalIssue { | |||
/** | |||
* The {@link RuleKey} of the issue. | |||
*/ | |||
NewExternalIssue forRule(RuleKey ruleKey); | |||
/** | |||
* Type of issue. | |||
*/ | |||
NewExternalIssue type(RuleType type); | |||
/** | |||
* Effort to fix the issue, in minutes. | |||
*/ | |||
NewExternalIssue remediationEffort(@Nullable Long effort); | |||
/** | |||
* Set the severity of the issue. | |||
*/ | |||
NewExternalIssue severity(Severity severity); | |||
/** | |||
* Primary location for this issue. | |||
*/ | |||
NewExternalIssue at(NewIssueLocation primaryLocation); | |||
/** | |||
* Add a secondary location for this issue. Several secondary locations can be registered. | |||
*/ | |||
NewExternalIssue addLocation(NewIssueLocation secondaryLocation); | |||
/** | |||
* Add a URL pointing to more information about the rule triggering this issue. | |||
*/ | |||
NewExternalIssue descriptionUrl(String url); | |||
/** | |||
* Register a flow for this issue. A flow is an ordered list of issue locations that help to understand the issue. | |||
* It should be a <b>path that backtracks the issue from its primary location to the start of the flow</b>. | |||
* Several flows can be registered. | |||
*/ | |||
NewExternalIssue addFlow(Iterable<NewIssueLocation> flowLocations); | |||
/** | |||
* Create a new location for this issue. First registered location is considered as primary location. | |||
*/ | |||
NewIssueLocation newLocation(); | |||
/** | |||
* Save the issue. If rule key is unknown or rule not enabled in the current quality profile then a warning is logged but no exception | |||
* is thrown. | |||
*/ | |||
void save(); | |||
} |
@@ -0,0 +1,95 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.api.batch.sensor.issue.internal; | |||
import com.google.common.base.Preconditions; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.List; | |||
import org.sonar.api.batch.sensor.internal.DefaultStorable; | |||
import org.sonar.api.batch.sensor.internal.SensorStorage; | |||
import org.sonar.api.batch.sensor.issue.IssueLocation; | |||
import org.sonar.api.batch.sensor.issue.NewIssueLocation; | |||
import org.sonar.api.batch.sensor.issue.Issue.Flow; | |||
import org.sonar.api.rule.RuleKey; | |||
import static com.google.common.base.Preconditions.checkState; | |||
import static java.util.Collections.unmodifiableList; | |||
import static java.util.stream.Collectors.toList; | |||
public abstract class AbstractDefaultIssue<T extends AbstractDefaultIssue> extends DefaultStorable { | |||
protected RuleKey ruleKey; | |||
protected IssueLocation primaryLocation; | |||
protected List<List<IssueLocation>> flows = new ArrayList<>(); | |||
protected AbstractDefaultIssue() { | |||
super(null); | |||
} | |||
public AbstractDefaultIssue(SensorStorage storage) { | |||
super(storage); | |||
} | |||
public T forRule(RuleKey ruleKey) { | |||
this.ruleKey = ruleKey; | |||
return (T) this; | |||
} | |||
public RuleKey ruleKey() { | |||
return this.ruleKey; | |||
} | |||
public IssueLocation primaryLocation() { | |||
return primaryLocation; | |||
} | |||
public List<Flow> flows() { | |||
return this.flows.stream() | |||
.<Flow>map(l -> () -> unmodifiableList(new ArrayList<>(l))) | |||
.collect(toList()); | |||
} | |||
public NewIssueLocation newLocation() { | |||
return new DefaultIssueLocation(); | |||
} | |||
public T at(NewIssueLocation primaryLocation) { | |||
Preconditions.checkArgument(primaryLocation != null, "Cannot use a location that is null"); | |||
checkState(this.primaryLocation == null, "at() already called"); | |||
this.primaryLocation = (DefaultIssueLocation) primaryLocation; | |||
Preconditions.checkArgument(this.primaryLocation.inputComponent() != null, "Cannot use a location with no input component"); | |||
return (T) this; | |||
} | |||
public T addLocation(NewIssueLocation secondaryLocation) { | |||
flows.add(Arrays.asList((IssueLocation) secondaryLocation)); | |||
return (T) this; | |||
} | |||
public T addFlow(Iterable<NewIssueLocation> locations) { | |||
List<IssueLocation> flowAsList = new ArrayList<>(); | |||
for (NewIssueLocation issueLocation : locations) { | |||
flowAsList.add((DefaultIssueLocation) issueLocation); | |||
} | |||
flows.add(flowAsList); | |||
return (T) this; | |||
} | |||
} |
@@ -0,0 +1,103 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.api.batch.sensor.issue.internal; | |||
import com.google.common.base.Preconditions; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.batch.rule.Severity; | |||
import org.sonar.api.batch.sensor.internal.SensorStorage; | |||
import org.sonar.api.batch.sensor.issue.ExternalIssue; | |||
import org.sonar.api.batch.sensor.issue.NewExternalIssue; | |||
import org.sonar.api.rules.RuleType; | |||
import static com.google.common.base.Preconditions.checkState; | |||
import static java.lang.String.format; | |||
import static java.util.Objects.requireNonNull; | |||
public class DefaultExternalIssue extends AbstractDefaultIssue<DefaultExternalIssue> implements ExternalIssue, NewExternalIssue { | |||
private Long effort; | |||
private Severity severity; | |||
private String url; | |||
private RuleType type; | |||
public DefaultExternalIssue() { | |||
super(null); | |||
} | |||
public DefaultExternalIssue(SensorStorage storage) { | |||
super(storage); | |||
} | |||
@Override | |||
public DefaultExternalIssue remediationEffort(@Nullable Long effort) { | |||
Preconditions.checkArgument(effort == null || effort >= 0, format("effort must be greater than or equal 0 (got %s)", effort)); | |||
this.effort = effort; | |||
return this; | |||
} | |||
@Override | |||
public DefaultExternalIssue severity(@Nullable Severity severity) { | |||
this.severity = severity; | |||
return this; | |||
} | |||
@Override | |||
public Severity severity() { | |||
return this.severity; | |||
} | |||
@Override | |||
public Long remediationEffort() { | |||
return this.effort; | |||
} | |||
@Override | |||
public void doSave() { | |||
requireNonNull(this.ruleKey, "ruleKey is mandatory on external issue"); | |||
checkState(primaryLocation != null, "Primary location is mandatory on every external issue"); | |||
checkState(primaryLocation.inputComponent().isFile(), "External issues must be located in files"); | |||
checkState(type != null, "Type is mandatory on every external issue"); | |||
checkState(severity != null, "Severity is mandatory on every external issue"); | |||
checkState(severity != null, "Severity is mandatory on every external issue"); | |||
storage.store(this); | |||
} | |||
@Override | |||
public DefaultExternalIssue descriptionUrl(String url) { | |||
this.url = url; | |||
return this; | |||
} | |||
@Override | |||
public String descriptionUrl() { | |||
return url; | |||
} | |||
@Override | |||
public RuleType type() { | |||
return type; | |||
} | |||
@Override | |||
public DefaultExternalIssue type(RuleType type) { | |||
this.type = type; | |||
return this; | |||
} | |||
} |
@@ -20,32 +20,20 @@ | |||
package org.sonar.api.batch.sensor.issue.internal; | |||
import com.google.common.base.Preconditions; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.List; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.batch.rule.Severity; | |||
import org.sonar.api.batch.sensor.internal.DefaultStorable; | |||
import org.sonar.api.batch.sensor.internal.SensorStorage; | |||
import org.sonar.api.batch.sensor.issue.Issue; | |||
import org.sonar.api.batch.sensor.issue.IssueLocation; | |||
import org.sonar.api.batch.sensor.issue.NewIssue; | |||
import org.sonar.api.batch.sensor.issue.NewIssueLocation; | |||
import org.sonar.api.rule.RuleKey; | |||
import static com.google.common.base.Preconditions.checkState; | |||
import static java.lang.String.format; | |||
import static java.util.Collections.unmodifiableList; | |||
import static java.util.Objects.requireNonNull; | |||
import static java.util.stream.Collectors.toList; | |||
public class DefaultIssue extends DefaultStorable implements Issue, NewIssue { | |||
private RuleKey ruleKey; | |||
public class DefaultIssue extends AbstractDefaultIssue<DefaultIssue> implements Issue, NewIssue { | |||
private Double gap; | |||
private Severity overriddenSeverity; | |||
private IssueLocation primaryLocation; | |||
private List<List<IssueLocation>> flows = new ArrayList<>(); | |||
public DefaultIssue() { | |||
super(null); | |||
@@ -55,12 +43,6 @@ public class DefaultIssue extends DefaultStorable implements Issue, NewIssue { | |||
super(storage); | |||
} | |||
@Override | |||
public DefaultIssue forRule(RuleKey ruleKey) { | |||
this.ruleKey = ruleKey; | |||
return this; | |||
} | |||
@Override | |||
public DefaultIssue effortToFix(@Nullable Double effortToFix) { | |||
return gap(effortToFix); | |||
@@ -79,41 +61,6 @@ public class DefaultIssue extends DefaultStorable implements Issue, NewIssue { | |||
return this; | |||
} | |||
@Override | |||
public NewIssueLocation newLocation() { | |||
return new DefaultIssueLocation(); | |||
} | |||
@Override | |||
public DefaultIssue at(NewIssueLocation primaryLocation) { | |||
Preconditions.checkArgument(primaryLocation != null, "Cannot use a location that is null"); | |||
checkState(this.primaryLocation == null, "at() already called"); | |||
this.primaryLocation = (DefaultIssueLocation) primaryLocation; | |||
Preconditions.checkArgument(this.primaryLocation.inputComponent() != null, "Cannot use a location with no input component"); | |||
return this; | |||
} | |||
@Override | |||
public NewIssue addLocation(NewIssueLocation secondaryLocation) { | |||
flows.add(Arrays.asList((IssueLocation) secondaryLocation)); | |||
return this; | |||
} | |||
@Override | |||
public DefaultIssue addFlow(Iterable<NewIssueLocation> locations) { | |||
List<IssueLocation> flowAsList = new ArrayList<>(); | |||
for (NewIssueLocation issueLocation : locations) { | |||
flowAsList.add((DefaultIssueLocation) issueLocation); | |||
} | |||
flows.add(flowAsList); | |||
return this; | |||
} | |||
@Override | |||
public RuleKey ruleKey() { | |||
return this.ruleKey; | |||
} | |||
@Override | |||
public Severity overriddenSeverity() { | |||
return this.overriddenSeverity; | |||
@@ -134,13 +81,6 @@ public class DefaultIssue extends DefaultStorable implements Issue, NewIssue { | |||
return primaryLocation; | |||
} | |||
@Override | |||
public List<Flow> flows() { | |||
return this.flows.stream() | |||
.<Flow>map(l -> () -> unmodifiableList(new ArrayList<>(l))) | |||
.collect(toList()); | |||
} | |||
@Override | |||
public void doSave() { | |||
requireNonNull(this.ruleKey, "ruleKey is mandatory on issue"); |
@@ -39,6 +39,7 @@ public class RuleKey implements Serializable, Comparable<RuleKey> { | |||
*/ | |||
@Deprecated | |||
public static final String MANUAL_REPOSITORY_KEY = "manual"; | |||
public static final String EXTERNAL_RULE_REPO_PREFIX = "external_"; | |||
private final String repository; | |||
private final String rule; |
@@ -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(); |
@@ -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(); | |||
} | |||
} |
@@ -20,6 +20,8 @@ | |||
package org.sonar.scanner.issue; | |||
import com.google.common.base.Strings; | |||
import java.util.Collection; | |||
import java.util.function.Consumer; | |||
import javax.annotation.concurrent.ThreadSafe; | |||
import org.sonar.api.batch.fs.TextRange; | |||
import org.sonar.api.batch.fs.internal.DefaultInputComponent; | |||
@@ -27,6 +29,7 @@ import org.sonar.api.batch.rule.ActiveRule; | |||
import org.sonar.api.batch.rule.ActiveRules; | |||
import org.sonar.api.batch.rule.Rule; | |||
import org.sonar.api.batch.rule.Rules; | |||
import org.sonar.api.batch.sensor.issue.ExternalIssue; | |||
import org.sonar.api.batch.sensor.issue.Issue; | |||
import org.sonar.api.batch.sensor.issue.Issue.Flow; | |||
import org.sonar.api.rule.RuleKey; | |||
@@ -73,6 +76,12 @@ public class ModuleIssues { | |||
return false; | |||
} | |||
public void initAndAddExternalIssue(ExternalIssue issue) { | |||
DefaultInputComponent inputComponent = (DefaultInputComponent) issue.primaryLocation().inputComponent(); | |||
ScannerReport.ExternalIssue rawExternalIssue = createReportExternalIssue(issue, inputComponent.batchId()); | |||
write(inputComponent.batchId(), rawExternalIssue); | |||
} | |||
private static ScannerReport.Issue createReportIssue(Issue issue, int componentRef, String ruleName, String activeRuleSeverity) { | |||
String primaryMessage = Strings.isNullOrEmpty(issue.primaryLocation().message()) ? ruleName : issue.primaryLocation().message(); | |||
org.sonar.api.batch.rule.Severity overriddenSeverity = issue.overriddenSeverity(); | |||
@@ -97,14 +106,41 @@ public class ModuleIssues { | |||
if (gap != null) { | |||
builder.setGap(gap); | |||
} | |||
applyFlows(componentRef, builder, locationBuilder, textRangeBuilder, issue); | |||
applyFlows(builder::addFlow, locationBuilder, textRangeBuilder, issue.flows()); | |||
return builder.build(); | |||
} | |||
private static void applyFlows(int componentRef, ScannerReport.Issue.Builder builder, ScannerReport.IssueLocation.Builder locationBuilder, | |||
ScannerReport.TextRange.Builder textRangeBuilder, Issue issue) { | |||
private static ScannerReport.ExternalIssue createReportExternalIssue(ExternalIssue issue, int componentRef) { | |||
String primaryMessage = issue.primaryLocation().message(); | |||
Severity severity = Severity.valueOf(issue.severity().name()); | |||
ScannerReport.ExternalIssue.Builder builder = ScannerReport.ExternalIssue.newBuilder(); | |||
ScannerReport.IssueLocation.Builder locationBuilder = IssueLocation.newBuilder(); | |||
ScannerReport.TextRange.Builder textRangeBuilder = ScannerReport.TextRange.newBuilder(); | |||
// non-null fields | |||
builder.setSeverity(severity); | |||
builder.setRuleRepository(issue.ruleKey().repository()); | |||
builder.setRuleKey(issue.ruleKey().rule()); | |||
builder.setMsg(primaryMessage); | |||
locationBuilder.setMsg(primaryMessage); | |||
locationBuilder.setComponentRef(componentRef); | |||
TextRange primaryTextRange = issue.primaryLocation().textRange(); | |||
if (primaryTextRange != null) { | |||
builder.setTextRange(toProtobufTextRange(textRangeBuilder, primaryTextRange)); | |||
} | |||
Long effort = issue.remediationEffort(); | |||
if (effort != null) { | |||
builder.setEffort(effort); | |||
} | |||
applyFlows(builder::addFlow, locationBuilder, textRangeBuilder, issue.flows()); | |||
return builder.build(); | |||
} | |||
private static void applyFlows(Consumer<ScannerReport.Flow> consumer, ScannerReport.IssueLocation.Builder locationBuilder, | |||
ScannerReport.TextRange.Builder textRangeBuilder, Collection<Flow> flows) { | |||
ScannerReport.Flow.Builder flowBuilder = ScannerReport.Flow.newBuilder(); | |||
for (Flow flow : issue.flows()) { | |||
for (Flow flow : flows) { | |||
if (flow.locations().isEmpty()) { | |||
return; | |||
} | |||
@@ -123,7 +159,7 @@ public class ModuleIssues { | |||
} | |||
flowBuilder.addLocation(locationBuilder.build()); | |||
} | |||
builder.addFlow(flowBuilder.build()); | |||
consumer.accept(flowBuilder.build()); | |||
} | |||
} | |||
@@ -152,4 +188,7 @@ public class ModuleIssues { | |||
reportPublisher.getWriter().appendComponentIssue(batchId, rawIssue); | |||
} | |||
public void write(int batchId, ScannerReport.ExternalIssue rawIssue) { | |||
reportPublisher.getWriter().appendComponentExternalIssue(batchId, rawIssue); | |||
} | |||
} |
@@ -123,6 +123,11 @@ public class TaskResult implements org.sonar.scanner.mediumtest.ScanTaskObserver | |||
int ref = reportComponents.get(inputComponent.key()).getRef(); | |||
return issuesFor(ref); | |||
} | |||
public List<ScannerReport.ExternalIssue> externalIssuesFor(InputComponent inputComponent) { | |||
int ref = reportComponents.get(inputComponent.key()).getRef(); | |||
return externalIssuesFor(ref); | |||
} | |||
public List<ScannerReport.Issue> issuesFor(Component reportComponent) { | |||
int ref = reportComponent.getRef(); | |||
@@ -138,6 +143,16 @@ public class TaskResult implements org.sonar.scanner.mediumtest.ScanTaskObserver | |||
} | |||
return result; | |||
} | |||
private List<ScannerReport.ExternalIssue> externalIssuesFor(int ref) { | |||
List<ScannerReport.ExternalIssue> result = Lists.newArrayList(); | |||
try (CloseableIterator<ScannerReport.ExternalIssue> it = reader.readComponentExternalIssues(ref)) { | |||
while (it.hasNext()) { | |||
result.add(it.next()); | |||
} | |||
} | |||
return result; | |||
} | |||
public Collection<InputFile> inputFiles() { | |||
return inputFiles.values(); |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -28,6 +28,7 @@ import org.sonar.api.batch.fs.internal.DefaultInputFile; | |||
import org.sonar.api.batch.fs.internal.TestInputFileBuilder; | |||
import org.sonar.api.batch.rule.internal.ActiveRulesBuilder; | |||
import org.sonar.api.batch.rule.internal.RulesBuilder; | |||
import org.sonar.api.batch.sensor.issue.internal.DefaultExternalIssue; | |||
import org.sonar.api.batch.sensor.issue.internal.DefaultIssue; | |||
import org.sonar.api.batch.sensor.issue.internal.DefaultIssueLocation; | |||
import org.sonar.api.rule.RuleKey; | |||
@@ -146,6 +147,23 @@ public class ModuleIssuesTest { | |||
assertThat(argument.getValue().getSeverity()).isEqualTo(org.sonar.scanner.protocol.Constants.Severity.CRITICAL); | |||
} | |||
@Test | |||
public void add_external_issue_to_cache() { | |||
ruleBuilder.add(SQUID_RULE_KEY).setName(SQUID_RULE_NAME); | |||
initModuleIssues(); | |||
DefaultExternalIssue issue = new DefaultExternalIssue() | |||
.at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message("Foo")) | |||
.forRule(SQUID_RULE_KEY) | |||
.severity(org.sonar.api.batch.rule.Severity.CRITICAL); | |||
moduleIssues.initAndAddExternalIssue(issue); | |||
ArgumentCaptor<ScannerReport.ExternalIssue> argument = ArgumentCaptor.forClass(ScannerReport.ExternalIssue.class); | |||
verify(reportPublisher.getWriter()).appendComponentExternalIssue(eq(file.batchId()), argument.capture()); | |||
assertThat(argument.getValue().getSeverity()).isEqualTo(org.sonar.scanner.protocol.Constants.Severity.CRITICAL); | |||
} | |||
@Test | |||
public void use_severity_from_active_rule_if_no_severity_on_issue() { | |||
ruleBuilder.add(SQUID_RULE_KEY).setName(SQUID_RULE_NAME); |
@@ -29,8 +29,10 @@ import org.junit.Test; | |||
import org.junit.rules.TemporaryFolder; | |||
import org.sonar.scanner.mediumtest.ScannerMediumTester; | |||
import org.sonar.scanner.mediumtest.TaskResult; | |||
import org.sonar.scanner.protocol.output.ScannerReport.ExternalIssue; | |||
import org.sonar.scanner.protocol.output.ScannerReport.Issue; | |||
import org.sonar.xoo.XooPlugin; | |||
import org.sonar.xoo.rule.OneExternalIssuePerLineSensor; | |||
import org.sonar.xoo.rule.XooRulesDefinition; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
@@ -60,10 +62,31 @@ public class IssuesMediumTest { | |||
List<Issue> issues = result.issuesFor(result.inputFile("xources/hello/HelloJava.xoo")); | |||
assertThat(issues).hasSize(8 /* lines */); | |||
List<ExternalIssue> externalIssues = result.externalIssuesFor(result.inputFile("xources/hello/HelloJava.xoo")); | |||
assertThat(externalIssues).isEmpty(); | |||
Issue issue = issues.get(0); | |||
assertThat(issue.getTextRange().getStartLine()).isEqualTo(issue.getTextRange().getStartLine()); | |||
} | |||
@Test | |||
public void testOneExternalIssuePerLine() throws Exception { | |||
File projectDir = new File(IssuesMediumTest.class.getResource("/mediumtest/xoo/sample").toURI()); | |||
File tmpDir = temp.newFolder(); | |||
FileUtils.copyDirectory(projectDir, tmpDir); | |||
TaskResult result = tester | |||
.newScanTask(new File(tmpDir, "sonar-project.properties")) | |||
.property(OneExternalIssuePerLineSensor.ACTIVATE_EXTERNAL_ISSUES, "true") | |||
.execute(); | |||
List<ExternalIssue> externalIssues = result.externalIssuesFor(result.inputFile("xources/hello/HelloJava.xoo")); | |||
assertThat(externalIssues).hasSize(8 /* lines */); | |||
ExternalIssue externalIssue = externalIssues.get(0); | |||
assertThat(externalIssue.getTextRange().getStartLine()).isEqualTo(externalIssue.getTextRange().getStartLine()); | |||
} | |||
@Test | |||
public void findActiveRuleByInternalKey() throws Exception { |
@@ -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(); |
@@ -36,7 +36,9 @@ import org.sonar.api.batch.fs.internal.TestInputFileBuilder; | |||
import org.sonar.api.batch.measure.MetricFinder; | |||
import org.sonar.api.batch.sensor.highlighting.TypeOfText; | |||
import org.sonar.api.batch.sensor.highlighting.internal.DefaultHighlighting; | |||
import org.sonar.api.batch.sensor.issue.ExternalIssue; | |||
import org.sonar.api.batch.sensor.issue.Issue; | |||
import org.sonar.api.batch.sensor.issue.internal.DefaultExternalIssue; | |||
import org.sonar.api.batch.sensor.issue.internal.DefaultIssue; | |||
import org.sonar.api.batch.sensor.issue.internal.DefaultIssueLocation; | |||
import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure; | |||
@@ -124,6 +126,18 @@ public class DefaultSensorStorageTest { | |||
assertThat(argumentCaptor.getValue()).isEqualTo(issue); | |||
} | |||
@Test | |||
public void should_save_external_issue() { | |||
InputFile file = new TestInputFileBuilder("foo", "src/Foo.php").build(); | |||
DefaultExternalIssue externalIssue = new DefaultExternalIssue().at(new DefaultIssueLocation().on(file)); | |||
underTest.store(externalIssue); | |||
ArgumentCaptor<ExternalIssue> argumentCaptor = ArgumentCaptor.forClass(ExternalIssue.class); | |||
verify(moduleIssues).initAndAddExternalIssue(argumentCaptor.capture()); | |||
assertThat(argumentCaptor.getValue()).isEqualTo(externalIssue); | |||
} | |||
@Test | |||
public void should_skip_issue_on_short_branch_when_file_status_is_SAME() { | |||
InputFile file = new TestInputFileBuilder("foo", "src/Foo.php").setStatus(InputFile.Status.SAME).build(); |
@@ -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), |
@@ -83,6 +83,14 @@ public class ScannerReportReader { | |||
return emptyCloseableIterator(); | |||
} | |||
public CloseableIterator<ScannerReport.ExternalIssue> readComponentExternalIssues(int componentRef) { | |||
File file = fileStructure.fileFor(FileStructure.Domain.EXTERNAL_ISSUES, componentRef); | |||
if (fileExists(file)) { | |||
return Protobuf.readStream(file, ScannerReport.ExternalIssue.parser()); | |||
} | |||
return emptyCloseableIterator(); | |||
} | |||
public CloseableIterator<ScannerReport.Duplication> readComponentDuplications(int componentRef) { | |||
File file = fileStructure.fileFor(FileStructure.Domain.DUPLICATIONS, componentRef); | |||
if (fileExists(file)) { |
@@ -82,6 +82,21 @@ public class ScannerReportWriter { | |||
} | |||
} | |||
public File writeComponentExternalIssues(int componentRef, Iterable<ScannerReport.ExternalIssue> issues) { | |||
File file = fileStructure.fileFor(FileStructure.Domain.EXTERNAL_ISSUES, componentRef); | |||
Protobuf.writeStream(issues, file, false); | |||
return file; | |||
} | |||
public void appendComponentExternalIssue(int componentRef, ScannerReport.ExternalIssue issue) { | |||
File file = fileStructure.fileFor(FileStructure.Domain.EXTERNAL_ISSUES, componentRef); | |||
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(file, true))) { | |||
issue.writeDelimitedTo(out); | |||
} catch (Exception e) { | |||
throw ContextException.of("Unable to write external issue", e).addContext("file", file); | |||
} | |||
} | |||
public File writeComponentMeasures(int componentRef, Iterable<ScannerReport.Measure> measures) { | |||
File file = fileStructure.fileFor(FileStructure.Domain.MEASURES, componentRef); | |||
Protobuf.writeStream(measures, file, false); |
@@ -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. |
@@ -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 |
@@ -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(); |
@@ -116,6 +116,26 @@ public class ScannerReportWriterTest { | |||
} | |||
} | |||
@Test | |||
public void write_external_issues() { | |||
// no data yet | |||
assertThat(underTest.hasComponentData(FileStructure.Domain.EXTERNAL_ISSUES, 1)).isFalse(); | |||
// write data | |||
ScannerReport.ExternalIssue issue = ScannerReport.ExternalIssue.newBuilder() | |||
.setMsg("the message") | |||
.build(); | |||
underTest.writeComponentExternalIssues(1, asList(issue)); | |||
assertThat(underTest.hasComponentData(FileStructure.Domain.EXTERNAL_ISSUES, 1)).isTrue(); | |||
File file = underTest.getFileStructure().fileFor(FileStructure.Domain.EXTERNAL_ISSUES, 1); | |||
assertThat(file).exists().isFile(); | |||
try (CloseableIterator<ScannerReport.ExternalIssue> read = Protobuf.readStream(file, ScannerReport.ExternalIssue.parser())) { | |||
assertThat(Iterators.size(read)).isEqualTo(1); | |||
} | |||
} | |||
@Test | |||
public void write_measures() { | |||
assertThat(underTest.hasComponentData(FileStructure.Domain.MEASURES, 1)).isFalse(); |