aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--build.gradle2
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java5
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java26
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCache.java2
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/protobuf/issue_cache.proto1
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java15
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java28
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java23
-rw-r--r--server/sonar-db-dao/src/main/protobuf/db-issues.proto2
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml12
-rw-r--r--server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java42
-rw-r--r--server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDtoTest.java29
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v93/DbVersion93.java1
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java42
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java72
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java12
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java24
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java18
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssueLocation.java44
-rw-r--r--sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultMessageFormatting.java71
-rw-r--r--sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssueTest.java25
-rw-r--r--sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultMessageFormattingTest.java65
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuePublisher.java19
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuePublisherTest.java7
-rw-r--r--sonar-scanner-protocol/src/main/protobuf/scanner_report.proto13
25 files changed, 538 insertions, 62 deletions
diff --git a/build.gradle b/build.gradle
index 1d0e3eefd1b..1f5320ca0cd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -201,7 +201,7 @@ subprojects {
dependency 'org.sonarsource.kotlin:sonar-kotlin-plugin:2.10.0.1456'
dependency 'org.sonarsource.slang:sonar-ruby-plugin:1.11.0.3905'
dependency 'org.sonarsource.slang:sonar-scala-plugin:1.11.0.3905'
- dependency 'org.sonarsource.api.plugin:sonar-plugin-api:9.12.0.310'
+ dependency 'org.sonarsource.api.plugin:sonar-plugin-api:9.13.0.351'
dependency 'org.sonarsource.xml:sonar-xml-plugin:2.6.1.3686'
dependency 'org.sonarsource.iac:sonar-iac-plugin:1.9.2.2279'
dependency 'org.sonarsource.text:sonar-text-plugin:1.1.0.282'
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java
index 475d7a477f6..881cf4e73ad 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java
@@ -62,7 +62,8 @@ public class IssueLifecycle {
this(analysisMetadataHolder, issueChangeContextByScanBuilder(new Date(analysisMetadataHolder.getAnalysisDate())).build(), workflow, updater, debtCalculator, ruleRepository);
}
- @VisibleForTesting IssueLifecycle(AnalysisMetadataHolder analysisMetadataHolder, IssueChangeContext changeContext, IssueWorkflow workflow, IssueFieldsSetter updater,
+ @VisibleForTesting
+ IssueLifecycle(AnalysisMetadataHolder analysisMetadataHolder, IssueChangeContext changeContext, IssueWorkflow workflow, IssueFieldsSetter updater,
DebtCalculator debtCalculator, RuleRepository ruleRepository) {
this.analysisMetadataHolder = analysisMetadataHolder;
this.workflow = workflow;
@@ -198,7 +199,7 @@ public class IssueLifecycle {
updater.setPastLine(raw, base.getLine());
updater.setPastLocations(raw, base.getLocations());
updater.setRuleDescriptionContextKey(raw, base.getRuleDescriptionContextKey().orElse(null));
- updater.setPastMessage(raw, base.getMessage(), changeContext);
+ updater.setPastMessage(raw, base.getMessage(), base.getMessageFormattings(), changeContext);
updater.setPastGap(raw, base.gap(), changeContext);
updater.setPastEffort(raw, base.effort(), changeContext);
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java
index d101ebf4250..1c7e085bb44 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java
@@ -26,6 +26,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
+import org.jetbrains.annotations.NotNull;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
import org.sonar.api.utils.Duration;
@@ -172,6 +173,9 @@ public class TrackerRawInputFactory {
}
if (isNotEmpty(reportIssue.getMsg())) {
issue.setMessage(reportIssue.getMsg());
+ if (!reportIssue.getMsgFormattingList().isEmpty()) {
+ issue.setMessageFormattings(convertMessageFormattings(reportIssue.getMsgFormattingList()));
+ }
} else {
Rule rule = ruleRepository.getByKey(ruleKey);
issue.setMessage(rule.getName());
@@ -227,6 +231,9 @@ public class TrackerRawInputFactory {
}
if (isNotEmpty(reportExternalIssue.getMsg())) {
issue.setMessage(reportExternalIssue.getMsg());
+ if (!reportExternalIssue.getMsgFormattingList().isEmpty()) {
+ issue.setMessageFormattings(convertMessageFormattings(reportExternalIssue.getMsgFormattingList()));
+ }
}
if (reportExternalIssue.getSeverity() != Severity.UNSET_SEVERITY) {
issue.setSeverity(reportExternalIssue.getSeverity().name());
@@ -308,6 +315,8 @@ public class TrackerRawInputFactory {
}
if (isNotEmpty(source.getMsg())) {
target.setMsg(source.getMsg());
+ source.getMsgFormattingList()
+ .forEach(m -> target.addMsgFormatting(convertMessageFormatting(m)));
}
if (source.hasTextRange()) {
ScannerReport.TextRange sourceRange = source.getTextRange();
@@ -326,4 +335,21 @@ public class TrackerRawInputFactory {
return targetRange;
}
}
+
+ private static DbIssues.MessageFormattings convertMessageFormattings(List<ScannerReport.MessageFormatting> msgFormattings) {
+ DbIssues.MessageFormattings.Builder builder = DbIssues.MessageFormattings.newBuilder();
+ msgFormattings.stream()
+ .forEach(m -> builder.addMessageFormatting(TrackerRawInputFactory.convertMessageFormatting(m)));
+ return builder.build();
+ }
+
+ @NotNull
+ private static DbIssues.MessageFormatting convertMessageFormatting(ScannerReport.MessageFormatting m) {
+ DbIssues.MessageFormatting.Builder msgFormattingBuilder = DbIssues.MessageFormatting.newBuilder();
+ return msgFormattingBuilder
+ .setStart(m.getStart())
+ .setEnd(m.getEnd())
+ .setType(DbIssues.MessageFormattingType.valueOf(m.getType().name())).build();
+ }
+
}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCache.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCache.java
index d6bd0fddb89..cc46d358b2b 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCache.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCache.java
@@ -106,6 +106,7 @@ public class ProtobufIssueDiskCache implements DiskCache<DefaultIssue> {
defaultIssue.setSeverity(next.hasSeverity() ? next.getSeverity() : null);
defaultIssue.setManualSeverity(next.getManualSeverity());
defaultIssue.setMessage(next.hasMessage() ? next.getMessage() : null);
+ defaultIssue.setMessageFormattings(next.hasMessageFormattings() ? next.getMessageFormattings() : null);
defaultIssue.setLine(next.hasLine() ? next.getLine() : null);
defaultIssue.setGap(next.hasGap() ? next.getGap() : null);
defaultIssue.setEffort(next.hasEffort() ? Duration.create(next.getEffort()) : null);
@@ -157,6 +158,7 @@ public class ProtobufIssueDiskCache implements DiskCache<DefaultIssue> {
ofNullable(defaultIssue.severity()).ifPresent(builder::setSeverity);
builder.setManualSeverity(defaultIssue.manualSeverity());
ofNullable(defaultIssue.message()).ifPresent(builder::setMessage);
+ ofNullable(defaultIssue.getMessageFormattings()).ifPresent(m -> builder.setMessageFormattings((DbIssues.MessageFormattings) m));
ofNullable(defaultIssue.line()).ifPresent(builder::setLine);
ofNullable(defaultIssue.gap()).ifPresent(builder::setGap);
ofNullable(defaultIssue.effort()).map(Duration::toMinutes).ifPresent(builder::setEffort);
diff --git a/server/sonar-ce-task-projectanalysis/src/main/protobuf/issue_cache.proto b/server/sonar-ce-task-projectanalysis/src/main/protobuf/issue_cache.proto
index f4135f33a39..1e29d4126c0 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/protobuf/issue_cache.proto
+++ b/server/sonar-ce-task-projectanalysis/src/main/protobuf/issue_cache.proto
@@ -80,6 +80,7 @@ message Issue {
optional bool isNewCodeReferenceIssue = 42;
optional bool isNoLongerNewCodeReferenceIssue = 43;
optional string ruleDescriptionContextKey = 44;
+ optional sonarqube.db.issues.MessageFormattings messageFormattings = 45;
}
message Comment {
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java
index cc9f0855902..232b9fa3165 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java
@@ -363,6 +363,16 @@ public class IssueLifecycleTest {
.setEndLine(12)
.build())
.build();
+
+ DbIssues.MessageFormattings messageFormattings = DbIssues.MessageFormattings.newBuilder()
+ .addMessageFormatting(DbIssues.MessageFormatting
+ .newBuilder()
+ .setStart(13)
+ .setEnd(17)
+ .setType(DbIssues.MessageFormattingType.CODE)
+ .build())
+ .build();
+
DefaultIssue base = new DefaultIssue()
.setKey("BASE_KEY")
.setCreationDate(parseDate("2015-01-01"))
@@ -376,7 +386,8 @@ public class IssueLifecycleTest {
.setOnDisabledRule(true)
.setSelectedAt(1000L)
.setLine(10)
- .setMessage("message")
+ .setMessage("message with code")
+ .setMessageFormattings(messageFormattings)
.setGap(15d)
.setRuleDescriptionContextKey("hibernate")
.setEffort(Duration.create(15L))
@@ -411,7 +422,7 @@ public class IssueLifecycleTest {
verify(updater).setPastSeverity(raw, BLOCKER, issueChangeContext);
verify(updater).setPastLine(raw, 10);
verify(updater).setRuleDescriptionContextKey(raw, "hibernate");
- verify(updater).setPastMessage(raw, "message", issueChangeContext);
+ verify(updater).setPastMessage(raw, "message with code", messageFormattings, issueChangeContext);
verify(updater).setPastEffort(raw, Duration.create(15L), issueChangeContext);
verify(updater).setPastLocations(raw, issueLocations);
}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java
index 44bb1724b0c..047a666a367 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java
@@ -62,6 +62,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.api.issue.Issue.STATUS_OPEN;
import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
+import static org.sonar.scanner.protocol.output.ScannerReport.MessageFormattingType.CODE;
@RunWith(DataProviderRunner.class)
public class TrackerRawInputFactoryTest {
@@ -128,6 +129,7 @@ public class TrackerRawInputFactoryTest {
ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
.setTextRange(newTextRange(2))
.setMsg("the message")
+ .addMsgFormatting(ScannerReport.MessageFormatting.newBuilder().setStart(0).setEnd(3).setType(CODE).build())
.setRuleRepository(ruleKey.repository())
.setRuleKey(ruleKey.rule())
.setSeverity(Constants.Severity.BLOCKER)
@@ -149,6 +151,13 @@ public class TrackerRawInputFactoryTest {
assertThat(issue.message()).isEqualTo("the message");
assertThat(issue.isQuickFixAvailable()).isTrue();
+ // Check message formatting
+ DbIssues.MessageFormattings messageFormattings = Iterators.getOnlyElement(issues.iterator()).getMessageFormattings();
+ assertThat(messageFormattings.getMessageFormattingCount()).isEqualTo(1);
+ assertThat(messageFormattings.getMessageFormatting(0).getStart()).isZero();
+ assertThat(messageFormattings.getMessageFormatting(0).getEnd()).isEqualTo(3);
+ assertThat(messageFormattings.getMessageFormatting(0).getType()).isEqualTo(DbIssues.MessageFormattingType.CODE);
+
// fields set by compute engine
assertThat(issue.checksum()).isEqualTo(input.getLineHashSequence().getHashForLine(2));
assertThat(issue.tags()).isEmpty();
@@ -162,6 +171,7 @@ public class TrackerRawInputFactoryTest {
RuleKey ruleKey = RuleKey.of("java", "S001");
markRuleAsActive(ruleKey);
+ ScannerReport.MessageFormatting messageFormatting = ScannerReport.MessageFormatting.newBuilder().setStart(0).setEnd(4).setType(CODE).build();
ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
.setMsg("the message")
.setRuleRepository(ruleKey.repository())
@@ -169,7 +179,7 @@ public class TrackerRawInputFactoryTest {
.addFlow(ScannerReport.Flow.newBuilder()
.setType(FlowType.DATA)
.setDescription("flow1")
- .addLocation(ScannerReport.IssueLocation.newBuilder().setMsg("loc1").setComponentRef(1).build())
+ .addLocation(ScannerReport.IssueLocation.newBuilder().setMsg("loc1").addMsgFormatting(messageFormatting).setComponentRef(1).build())
.addLocation(ScannerReport.IssueLocation.newBuilder().setMsg("loc2").setComponentRef(1).build()))
.addFlow(ScannerReport.Flow.newBuilder()
.setType(FlowType.EXECUTION)
@@ -187,6 +197,11 @@ public class TrackerRawInputFactoryTest {
assertThat(locations.getFlow(0).getType()).isEqualTo(DbIssues.FlowType.DATA);
assertThat(locations.getFlow(0).getLocationList()).hasSize(2);
+ assertThat(locations.getFlow(0).getLocation(0).getMsg()).isEqualTo("loc1");
+ assertThat(locations.getFlow(0).getLocation(0).getMsgFormattingCount()).isEqualTo(1);
+ assertThat(locations.getFlow(0).getLocation(0).getMsgFormatting(0)).extracting(m -> m.getStart(), m -> m.getEnd(), m -> m.getType())
+ .containsExactly(0, 4, DbIssues.MessageFormattingType.CODE);
+
assertThat(locations.getFlow(1).hasDescription()).isFalse();
assertThat(locations.getFlow(1).getType()).isEqualTo(DbIssues.FlowType.EXECUTION);
assertThat(locations.getFlow(1).getLocationList()).hasSize(1);
@@ -293,6 +308,7 @@ public class TrackerRawInputFactoryTest {
ScannerReport.ExternalIssue reportIssue = ScannerReport.ExternalIssue.newBuilder()
.setTextRange(newTextRange(2))
.setMsg("the message")
+ .addMsgFormatting(ScannerReport.MessageFormatting.newBuilder().setStart(0).setEnd(3).build())
.setEngineId("eslint")
.setRuleId("S001")
.setSeverity(Constants.Severity.BLOCKER)
@@ -313,6 +329,14 @@ public class TrackerRawInputFactoryTest {
assertThat(issue.line()).isEqualTo(2);
assertThat(issue.effort()).isEqualTo(Duration.create(20L));
assertThat(issue.message()).isEqualTo("the message");
+
+ // Check message formatting
+ DbIssues.MessageFormattings messageFormattings = Iterators.getOnlyElement(issues.iterator()).getMessageFormattings();
+ assertThat(messageFormattings.getMessageFormattingCount()).isEqualTo(1);
+ assertThat(messageFormattings.getMessageFormatting(0).getStart()).isZero();
+ assertThat(messageFormattings.getMessageFormatting(0).getEnd()).isEqualTo(3);
+ assertThat(messageFormattings.getMessageFormatting(0).getType()).isEqualTo(DbIssues.MessageFormattingType.CODE);
+
assertThat(issue.type()).isEqualTo(expectedRuleType);
DbIssues.Locations locations = Iterators.getOnlyElement(issues.iterator()).getLocations();
@@ -320,8 +344,6 @@ public class TrackerRawInputFactoryTest {
assertThat(locations.getFlow(0).getType()).isEqualTo(DbIssues.FlowType.DATA);
assertThat(locations.getFlow(0).getLocationList()).hasSize(1);
-
-
// fields set by compute engine
assertThat(issue.checksum()).isEqualTo(input.getLineHashSequence().getHashForLine(2));
assertThat(issue.tags()).isEmpty();
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java
index 1cbc7ee2660..55dffc5b7c2 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java
@@ -115,7 +115,7 @@ public final class IssueDto implements Serializable {
.setLine(issue.line())
.setLocations((DbIssues.Locations) issue.getLocations())
.setMessage(issue.message())
- .setMessageFormattings((byte[]) null)
+ .setMessageFormattings((DbIssues.MessageFormattings) issue.getMessageFormattings())
.setGap(issue.gap())
.setEffort(issue.effortInMinutes())
.setResolution(issue.resolution())
@@ -142,7 +142,6 @@ public final class IssueDto implements Serializable {
.setQuickFixAvailable(issue.isQuickFixAvailable())
.setIsNewCodeReferenceIssue(issue.isNewCodeReferenceIssue())
-
// technical dates
.setCreatedAt(now)
.setUpdatedAt(now);
@@ -165,7 +164,7 @@ public final class IssueDto implements Serializable {
.setLine(issue.line())
.setLocations((DbIssues.Locations) issue.getLocations())
.setMessage(issue.message())
- .setMessageFormattings((byte[]) null)
+ .setMessageFormattings((DbIssues.MessageFormattings) issue.getMessageFormattings())
.setGap(issue.gap())
.setEffort(issue.effortInMinutes())
.setResolution(issue.resolution())
@@ -265,7 +264,11 @@ public final class IssueDto implements Serializable {
return this;
}
- public IssueDto setMessageFormattings(@Nullable byte[] messageFormattings) {
+ public byte[] getMessageFormattings() {
+ return messageFormattings;
+ }
+
+ public IssueDto setMessageFormattings(byte[] messageFormattings) {
this.messageFormattings = messageFormattings;
return this;
}
@@ -280,8 +283,15 @@ public final class IssueDto implements Serializable {
}
@CheckForNull
- public byte[] getMessageFormattings() {
- return messageFormattings;
+ public DbIssues.MessageFormattings parseMessageFormattings() {
+ if (messageFormattings != null) {
+ try {
+ return DbIssues.MessageFormattings.parseFrom(messageFormattings);
+ } catch (InvalidProtocolBufferException e) {
+ throw new IllegalStateException(String.format("Fail to read ISSUES.MESSAGE_FORMATTINGS [KEE=%s]", kee), e);
+ }
+ }
+ return null;
}
@CheckForNull
@@ -744,6 +754,7 @@ public final class IssueDto implements Serializable {
issue.setStatus(status);
issue.setResolution(resolution);
issue.setMessage(message);
+ issue.setMessageFormattings(parseMessageFormattings());
issue.setGap(gap);
issue.setEffort(effort != null ? Duration.create(effort) : null);
issue.setLine(line);
diff --git a/server/sonar-db-dao/src/main/protobuf/db-issues.proto b/server/sonar-db-dao/src/main/protobuf/db-issues.proto
index 8672388eecc..7632d34656a 100644
--- a/server/sonar-db-dao/src/main/protobuf/db-issues.proto
+++ b/server/sonar-db-dao/src/main/protobuf/db-issues.proto
@@ -54,7 +54,7 @@ message Location {
optional sonarqube.db.commons.TextRange text_range = 2;
optional string msg = 3;
optional string checksum = 4;
- repeated MessageFormatting msgFormattings = 5;
+ repeated MessageFormatting msgFormatting = 5;
}
message MessageFormattings {
repeated MessageFormatting messageFormatting = 1;
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
index ae2e340fa64..84824708ea9 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
@@ -10,6 +10,7 @@
i.severity as severity,
i.manual_severity as manualSeverity,
i.message as message,
+ i.message_formattings as messageFormattings,
i.line as line,
i.locations as locations,
i.gap as gap,
@@ -48,6 +49,7 @@
i.severity,
i.manual_severity,
i.message,
+ i.message_formattings,
i.line,
i.locations,
i.gap,
@@ -107,14 +109,17 @@
<insert id="insert" parameterType="Issue" useGeneratedKeys="false">
INSERT INTO issues (kee, rule_uuid, severity, manual_severity,
- message, line, locations, gap, effort, status, tags, rule_description_context_key,
+ message, message_formattings, line, locations, gap, effort, status, tags, rule_description_context_key,
resolution, checksum, assignee, author_login, issue_creation_date, issue_update_date,
issue_close_date, created_at, updated_at, component_uuid, project_uuid, issue_type, quick_fix_available)
VALUES (
#{kee,jdbcType=VARCHAR},
#{ruleUuid,jdbcType=VARCHAR},
#{severity,jdbcType=VARCHAR},
- #{manualSeverity,jdbcType=BOOLEAN}, #{message,jdbcType=VARCHAR}, #{line,jdbcType=INTEGER},
+ #{manualSeverity,jdbcType=BOOLEAN},
+ #{message,jdbcType=VARCHAR},
+ #{messageFormattings,jdbcType=BINARY},
+ #{line,jdbcType=INTEGER},
#{locations,jdbcType=BINARY},
#{gap,jdbcType=DOUBLE}, #{effort,jdbcType=INTEGER}, #{status,jdbcType=VARCHAR},
#{tagsString,jdbcType=VARCHAR},
@@ -150,6 +155,7 @@
severity=#{severity,jdbcType=VARCHAR},
manual_severity=#{manualSeverity,jdbcType=BOOLEAN},
message=#{message,jdbcType=VARCHAR},
+ message_formattings=#{messageFormattings,jdbcType=BINARY},
line=#{line,jdbcType=INTEGER},
locations=#{locations,jdbcType=BINARY},
gap=#{gap,jdbcType=DOUBLE},
@@ -178,6 +184,7 @@
severity=#{severity,jdbcType=VARCHAR},
manual_severity=#{manualSeverity,jdbcType=BOOLEAN},
message=#{message,jdbcType=VARCHAR},
+ message_formattings=#{messageFormattings,jdbcType=BINARY},
line=#{line,jdbcType=INTEGER},
locations=#{locations,jdbcType=BINARY},
gap=#{gap,jdbcType=DOUBLE},
@@ -749,6 +756,7 @@
r.plugin_name as ruleRepo,
r.plugin_rule_key as ruleKey,
i.message as message,
+ i.message_formattings as messageFormattings,
i.severity as severity,
i.manual_severity as manualSeverity,
i.issue_type as type,
diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java
index 598248c39a7..f575c82df67 100644
--- a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java
+++ b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java
@@ -37,6 +37,7 @@ import org.sonar.db.component.BranchType;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentTesting;
import org.sonar.db.component.ComponentUpdateDto;
+import org.sonar.db.protobuf.DbIssues;
import org.sonar.db.rule.RuleDto;
import org.sonar.db.rule.RuleTesting;
@@ -62,6 +63,7 @@ import static org.sonar.db.component.ComponentTesting.newDirectory;
import static org.sonar.db.component.ComponentTesting.newFileDto;
import static org.sonar.db.component.ComponentTesting.newModuleDto;
import static org.sonar.db.issue.IssueTesting.newCodeReferenceIssue;
+import static org.sonar.db.protobuf.DbIssues.MessageFormattingType.CODE;
public class IssueDaoTest {
@@ -77,6 +79,13 @@ public class IssueDaoTest {
private static final RuleType[] RULE_TYPES_EXCEPT_HOTSPOT = Stream.of(RuleType.values())
.filter(r -> r != RuleType.SECURITY_HOTSPOT)
.toArray(RuleType[]::new);
+ private static final DbIssues.MessageFormattings MESSAGE_FORMATTING = DbIssues.MessageFormattings.newBuilder()
+ .addMessageFormatting(DbIssues.MessageFormatting.newBuilder()
+ .setStart(0)
+ .setEnd(4)
+ .setType(CODE)
+ .build())
+ .build();
@Rule
public DbTester db = DbTester.create(System2.INSTANCE);
@@ -97,6 +106,7 @@ public class IssueDaoTest {
assertThat(issue.getType()).isEqualTo(2);
assertThat(issue.isManualSeverity()).isFalse();
assertThat(issue.getMessage()).isEqualTo("the message");
+ assertThat(issue.parseMessageFormattings()).isEqualTo(MESSAGE_FORMATTING);
assertThat(issue.getOptionalRuleDescriptionContextKey()).contains(TEST_CONTEXT_KEY);
assertThat(issue.getLine()).isEqualTo(500);
assertThat(issue.getEffort()).isEqualTo(10L);
@@ -221,7 +231,6 @@ public class IssueDaoTest {
assertThat(issues).contains("I1");
}
-
@Test
public void selectByBranch() {
long updatedAt = 1_340_000_000_000L;
@@ -250,8 +259,11 @@ public class IssueDaoTest {
.containsExactlyInAnyOrder(
tuple("issueA0", STATUS_OPEN),
tuple("issueA1", STATUS_REVIEWED),
- tuple("issueA3", STATUS_RESOLVED)
- );
+ tuple("issueA3", STATUS_RESOLVED));
+
+ assertThat(branchAIssuesA1.get(0))
+ .extracting(IssueDto::getMessage, IssueDto::parseMessageFormattings)
+ .containsOnly("message", MESSAGE_FORMATTING);
List<IssueDto> branchAIssuesA2 = underTest.selectByBranch(db.getSession(), Set.of("issueA0", "issueA1", "issueA3"),
buildSelectByBranchQuery(branchA, "java", true, changedSince));
@@ -268,8 +280,7 @@ public class IssueDaoTest {
.extracting(IssueDto::getKey, IssueDto::getStatus)
.containsExactlyInAnyOrder(
tuple("issueB0", STATUS_OPEN),
- tuple("issueB1", STATUS_RESOLVED)
- );
+ tuple("issueB1", STATUS_RESOLVED));
List<IssueDto> branchBIssuesB2 = underTest.selectByBranch(db.getSession(), Set.of("issueB0", "issueB1"), buildSelectByBranchQuery(branchB, "java", true, changedSince));
@@ -307,11 +318,11 @@ public class IssueDaoTest {
assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), file.uuid()))
.extracting(IssueDto::getKey)
- .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile}).map(IssueDto::getKey).toArray(String[]::new));
+ .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile}).map(IssueDto::getKey).toArray(String[]::new));
assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), project.uuid()))
.extracting(IssueDto::getKey)
- .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
+ .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), "does_not_exist")).isEmpty();
}
@@ -339,11 +350,11 @@ public class IssueDaoTest {
assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), project))
.extracting(IssueDto::getKey)
.containsExactlyInAnyOrder(
- Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile, openIssueOnModule, openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
+ Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile, openIssueOnModule, openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), module))
.extracting(IssueDto::getKey)
- .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile, openIssueOnModule}).map(IssueDto::getKey).toArray(String[]::new));
+ .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile, openIssueOnModule}).map(IssueDto::getKey).toArray(String[]::new));
ComponentDto notPersisted = ComponentTesting.newPrivateProjectDto();
assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), notPersisted)).isEmpty();
@@ -511,7 +522,7 @@ public class IssueDaoTest {
db.issues().insert(rule, project, file,
i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG));
- //two issues part of new code period on reference branch
+ // two issues part of new code period on reference branch
db.issues().insertNewCodeReferenceIssue(fpBug);
db.issues().insertNewCodeReferenceIssue(criticalBug1);
db.issues().insertNewCodeReferenceIssue(criticalBug2);
@@ -733,8 +744,7 @@ public class IssueDaoTest {
public void selectByKey_givenOneIssueWithoutRuleDescriptionContextKey_returnsEmptyOptional() {
prepareIssuesComponent();
underTest.insert(db.getSession(), createIssueWithKey(ISSUE_KEY1)
- .setRuleDescriptionContextKey(null)
- );
+ .setRuleDescriptionContextKey(null));
IssueDto issue1 = underTest.selectOrFailByKey(db.getSession(), ISSUE_KEY1);
assertThat(issue1.getOptionalRuleDescriptionContextKey()).isEmpty();
@@ -744,8 +754,7 @@ public class IssueDaoTest {
public void selectByKey_givenOneIssueWithRuleDescriptionContextKey_returnsContextKey() {
prepareIssuesComponent();
underTest.insert(db.getSession(), createIssueWithKey(ISSUE_KEY1)
- .setRuleDescriptionContextKey(TEST_CONTEXT_KEY)
- );
+ .setRuleDescriptionContextKey(TEST_CONTEXT_KEY));
IssueDto issue1 = underTest.selectOrFailByKey(db.getSession(), ISSUE_KEY1);
@@ -823,6 +832,7 @@ public class IssueDaoTest {
dto.setAssigneeUuid("karadoc");
dto.setChecksum("123456789");
dto.setMessage("the message");
+ dto.setMessageFormattings(MESSAGE_FORMATTING);
dto.setRuleDescriptionContextKey(TEST_CONTEXT_KEY);
dto.setCreatedAt(1_440_000_000_000L);
dto.setUpdatedAt(1_440_000_000_000L);
@@ -858,7 +868,9 @@ public class IssueDaoTest {
}
private void insertBranchIssue(ComponentDto branch, ComponentDto file, RuleDto rule, String id, String status, Long updateAt) {
- db.issues().insert(rule, branch, file, i -> i.setKee("issue" + id).setStatus(status).setUpdatedAt(updateAt).setType(randomRuleTypeExceptHotspot()));
+ db.issues().insert(rule, branch, file, i -> i.setKee("issue" + id).setStatus(status).setUpdatedAt(updateAt).setType(randomRuleTypeExceptHotspot())
+ .setMessage("message")
+ .setMessageFormattings(MESSAGE_FORMATTING));
}
private static IssueQueryParams buildSelectByBranchQuery(ComponentDto branch, String language, boolean resolvedOnly, Long changedSince) {
diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDtoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDtoTest.java
index d90e7fc671f..23580085d26 100644
--- a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDtoTest.java
+++ b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDtoTest.java
@@ -43,7 +43,8 @@ public class IssueDtoTest {
private static final DbIssues.MessageFormattings EXAMPLE_MESSAGE_FORMATTINGS = DbIssues.MessageFormattings.newBuilder()
.addMessageFormatting(DbIssues.MessageFormatting.newBuilder().setStart(0).setEnd(1).setType(DbIssues.MessageFormattingType.CODE)
- .build()).build();
+ .build())
+ .build();
@Test
public void toDefaultIssue_set_issue_fields() {
@@ -95,7 +96,7 @@ public class IssueDtoTest {
assertThat(issue.line()).isEqualTo(6);
assertThat(issue.severity()).isEqualTo("BLOCKER");
assertThat(issue.message()).isEqualTo("message");
- //assertThat(issue.getMessageFormatting()).isEqualTo(EXAMPLE_MESSAGE_FORMATTING); //TODO fix later SONAR-17592
+ assertThat((DbIssues.MessageFormattings) issue.getMessageFormattings()).isEqualTo(EXAMPLE_MESSAGE_FORMATTINGS);
assertThat(issue.manualSeverity()).isTrue();
assertThat(issue.assignee()).isEqualTo("perceval");
assertThat(issue.authorLogin()).isEqualTo("pierre");
@@ -153,24 +154,23 @@ public class IssueDtoTest {
IssueDto issueDto = IssueDto.toDtoForComputationInsert(defaultIssue, "ruleUuid", now);
- assertThat(issueDto).extracting(IssueDto::getKey, IssueDto::getType, IssueDto::getRuleKey).
- containsExactly("key", RuleType.BUG.getDbConstant(), RuleKey.of("repo", "rule"));
+ assertThat(issueDto).extracting(IssueDto::getKey, IssueDto::getType, IssueDto::getRuleKey).containsExactly("key", RuleType.BUG.getDbConstant(), RuleKey.of("repo", "rule"));
assertThat(issueDto).extracting(IssueDto::getIssueCreationDate, IssueDto::getIssueCloseDate,
- IssueDto::getIssueUpdateDate, IssueDto::getSelectedAt, IssueDto::getUpdatedAt, IssueDto::getCreatedAt)
+ IssueDto::getIssueUpdateDate, IssueDto::getSelectedAt, IssueDto::getUpdatedAt, IssueDto::getCreatedAt)
.containsExactly(dateNow, dateNow, dateNow, dateNow.getTime(), now, now);
assertThat(issueDto).extracting(IssueDto::getLine, IssueDto::getMessage,
- IssueDto::getGap, IssueDto::getEffort, IssueDto::getResolution, IssueDto::getStatus, IssueDto::getSeverity)
+ IssueDto::getGap, IssueDto::getEffort, IssueDto::getResolution, IssueDto::getStatus, IssueDto::getSeverity)
.containsExactly(1, "message", 1.0, 1L, Issue.RESOLUTION_FALSE_POSITIVE, Issue.STATUS_CLOSED, "BLOCKER");
assertThat(issueDto).extracting(IssueDto::getTags, IssueDto::getAuthorLogin)
.containsExactly(Set.of("todo"), "admin");
assertThat(issueDto).extracting(IssueDto::isManualSeverity, IssueDto::getChecksum, IssueDto::getAssigneeUuid,
- IssueDto::isExternal, IssueDto::getComponentUuid, IssueDto::getComponentKey,
- IssueDto::getModuleUuidPath, IssueDto::getProjectUuid, IssueDto::getProjectKey,
- IssueDto::getRuleUuid)
+ IssueDto::isExternal, IssueDto::getComponentUuid, IssueDto::getComponentKey,
+ IssueDto::getModuleUuidPath, IssueDto::getProjectUuid, IssueDto::getProjectKey,
+ IssueDto::getRuleUuid)
.containsExactly(true, "123", "123", true, "123", "componentKey",
"path/to/module/uuid", "123", "projectKey", "ruleUuid");
@@ -187,23 +187,22 @@ public class IssueDtoTest {
IssueDto issueDto = IssueDto.toDtoForUpdate(defaultIssue, now);
- assertThat(issueDto).extracting(IssueDto::getKey, IssueDto::getType, IssueDto::getRuleKey).
- containsExactly("key", RuleType.BUG.getDbConstant(), RuleKey.of("repo", "rule"));
+ assertThat(issueDto).extracting(IssueDto::getKey, IssueDto::getType, IssueDto::getRuleKey).containsExactly("key", RuleType.BUG.getDbConstant(), RuleKey.of("repo", "rule"));
assertThat(issueDto).extracting(IssueDto::getIssueCreationDate, IssueDto::getIssueCloseDate,
- IssueDto::getIssueUpdateDate, IssueDto::getSelectedAt, IssueDto::getUpdatedAt)
+ IssueDto::getIssueUpdateDate, IssueDto::getSelectedAt, IssueDto::getUpdatedAt)
.containsExactly(dateNow, dateNow, dateNow, dateNow.getTime(), now);
assertThat(issueDto).extracting(IssueDto::getLine, IssueDto::getMessage,
- IssueDto::getGap, IssueDto::getEffort, IssueDto::getResolution, IssueDto::getStatus, IssueDto::getSeverity)
+ IssueDto::getGap, IssueDto::getEffort, IssueDto::getResolution, IssueDto::getStatus, IssueDto::getSeverity)
.containsExactly(1, "message", 1.0, 1L, Issue.RESOLUTION_FALSE_POSITIVE, Issue.STATUS_CLOSED, "BLOCKER");
assertThat(issueDto).extracting(IssueDto::getTags, IssueDto::getAuthorLogin)
.containsExactly(Set.of("todo"), "admin");
assertThat(issueDto).extracting(IssueDto::isManualSeverity, IssueDto::getChecksum, IssueDto::getAssigneeUuid,
- IssueDto::isExternal, IssueDto::getComponentUuid, IssueDto::getComponentKey,
- IssueDto::getModuleUuidPath, IssueDto::getProjectUuid, IssueDto::getProjectKey)
+ IssueDto::isExternal, IssueDto::getComponentUuid, IssueDto::getComponentKey,
+ IssueDto::getModuleUuidPath, IssueDto::getProjectUuid, IssueDto::getProjectKey)
.containsExactly(true, "123", "123", true, "123", "componentKey",
"path/to/module/uuid", "123", "projectKey");
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v93/DbVersion93.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v93/DbVersion93.java
index eae98035f7d..c469735058f 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v93/DbVersion93.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v93/DbVersion93.java
@@ -23,6 +23,7 @@ import org.sonar.server.platform.db.migration.step.MigrationStepRegistry;
import org.sonar.server.platform.db.migration.version.DbVersion;
public class DbVersion93 implements DbVersion {
+
@Override
public void addSteps(MigrationStepRegistry registry) {
registry
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java
index 1b7afe26d32..a6d2b6e0024 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java
@@ -288,10 +288,48 @@ public class IssueFieldsSetter {
return false;
}
- public boolean setPastMessage(DefaultIssue issue, @Nullable String previousMessage, IssueChangeContext context) {
+ public boolean setMessageFormattings(DefaultIssue issue, @Nullable Object issueMessageFormattings, IssueChangeContext context) {
+ if (!messageFormattingsEqualsIgnoreHashes(issueMessageFormattings, issue.getMessageFormattings())) {
+ issue.setMessageFormattings(issueMessageFormattings);
+ issue.setUpdateDate(context.date());
+ issue.setChanged(true);
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean messageFormattingsEqualsIgnoreHashes(@Nullable Object l1, @Nullable DbIssues.MessageFormattings l2) {
+ if (l1 == null && l2 == null) {
+ return true;
+ }
+
+ if (l2 == null || !(l1 instanceof DbIssues.MessageFormattings)) {
+ return false;
+ }
+
+ DbIssues.MessageFormattings l1c = (DbIssues.MessageFormattings) l1;
+
+ if (!Objects.equals(l1c.getMessageFormattingCount(), l2.getMessageFormattingCount())) {
+ return false;
+ }
+
+ for (int i = 0; i < l1c.getMessageFormattingCount(); i++) {
+ if (l1c.getMessageFormatting(i).getStart() != l2.getMessageFormatting(i).getStart()
+ || l1c.getMessageFormatting(i).getEnd() != l2.getMessageFormatting(i).getEnd()
+ || l1c.getMessageFormatting(i).getType() != l2.getMessageFormatting(i).getType()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public boolean setPastMessage(DefaultIssue issue, @Nullable String previousMessage, @Nullable Object previousMessageFormattings, IssueChangeContext context) {
String currentMessage = issue.message();
+ DbIssues.MessageFormattings currentMessageFormattings = issue.getMessageFormattings();
issue.setMessage(previousMessage);
- return setMessage(issue, currentMessage, context);
+ issue.setMessageFormattings(previousMessageFormattings);
+ boolean changed = setMessage(issue, currentMessage, context);
+ return setMessageFormattings(issue, currentMessageFormattings, context) || changed;
}
public void addComment(DefaultIssue issue, String text, IssueChangeContext context) {
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java
index a2a0053159a..4c434971b47 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java
@@ -21,6 +21,7 @@ package org.sonar.server.issue;
import java.util.Calendar;
import java.util.Date;
+import java.util.List;
import java.util.Random;
import org.apache.commons.lang.time.DateUtils;
import org.junit.Test;
@@ -30,11 +31,13 @@ import org.sonar.core.issue.FieldDiffs;
import org.sonar.core.issue.IssueChangeContext;
import org.sonar.db.protobuf.DbCommons;
import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.protobuf.DbIssues.MessageFormattingType;
import org.sonar.db.user.UserDto;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.sonar.core.issue.IssueChangeContext.issueChangeContextByUserBuilder;
+import static org.sonar.db.protobuf.DbIssues.MessageFormattingType.CODE;
import static org.sonar.db.user.UserTesting.newUserDto;
import static org.sonar.server.issue.IssueFieldsSetter.ASSIGNEE;
import static org.sonar.server.issue.IssueFieldsSetter.RESOLUTION;
@@ -500,7 +503,7 @@ public class IssueFieldsSetterTest {
@Test
public void set_past_message() {
issue.setMessage("new message");
- boolean updated = underTest.setPastMessage(issue, "past message", context);
+ boolean updated = underTest.setPastMessage(issue, "past message", null, context);
assertThat(updated).isTrue();
assertThat(issue.message()).isEqualTo("new message");
@@ -510,6 +513,73 @@ public class IssueFieldsSetterTest {
}
@Test
+ public void set_past_message_formatting() {
+ issue.setMessage("past message");
+ DbIssues.MessageFormattings newFormatting = formattings(formatting(0, 3, CODE));
+ DbIssues.MessageFormattings pastFormatting = formattings(formatting(0, 7, CODE));
+ issue.setMessageFormattings(newFormatting);
+ boolean updated = underTest.setPastMessage(issue, "past message", pastFormatting, context);
+ assertThat(updated).isTrue();
+ assertThat(issue.message()).isEqualTo("past message");
+ assertThat((DbIssues.MessageFormattings) issue.getMessageFormattings()).isEqualTo(newFormatting);
+
+ // do not save change
+ assertThat(issue.currentChange()).isNull();
+ assertThat(issue.mustSendNotifications()).isFalse();
+ }
+
+ @Test
+ public void set_past_message_formatting_no_changes() {
+ issue.setMessage("past message");
+ DbIssues.MessageFormattings sameFormatting = formattings(formatting(0, 3, CODE));
+ issue.setMessageFormattings(sameFormatting);
+ boolean updated = underTest.setPastMessage(issue, "past message", sameFormatting, context);
+ assertThat(updated).isFalse();
+ assertThat(issue.message()).isEqualTo("past message");
+ assertThat((DbIssues.MessageFormattings) issue.getMessageFormattings()).isEqualTo(sameFormatting);
+
+ // do not save change
+ assertThat(issue.currentChange()).isNull();
+ assertThat(issue.mustSendNotifications()).isFalse();
+ }
+
+ @Test
+ public void message_formatting_different_size_is_changed(){
+ issue.setMessageFormattings(formattings(formatting(0,3,CODE)));
+ boolean updated = underTest.setLocations(issue, formattings(formatting(0,3,CODE), formatting(4,6,CODE)));
+ assertThat(updated).isTrue();
+ }
+
+ @Test
+ public void message_formatting_different_start_is_changed(){
+ issue.setMessageFormattings(formattings(formatting(0,3,CODE)));
+ boolean updated = underTest.setLocations(issue, formattings(formatting(1,3,CODE)));
+ assertThat(updated).isTrue();
+ }
+
+ @Test
+ public void message_formatting_different_end_is_changed(){
+ issue.setMessageFormattings(formattings(formatting(0,3,CODE)));
+ boolean updated = underTest.setLocations(issue, formattings(formatting(0,4,CODE)));
+ assertThat(updated).isTrue();
+ }
+
+ private static DbIssues.MessageFormatting formatting(int start, int end, MessageFormattingType type) {
+ return DbIssues.MessageFormatting
+ .newBuilder()
+ .setStart(start)
+ .setEnd(end)
+ .setType(type)
+ .build();
+ }
+
+ private static DbIssues.MessageFormattings formattings(DbIssues.MessageFormatting... messageFormatting) {
+ return DbIssues.MessageFormattings.newBuilder()
+ .addAllMessageFormatting(List.of(messageFormatting))
+ .build();
+ }
+
+ @Test
public void set_author() {
boolean updated = underTest.setAuthorLogin(issue, "eric", context);
assertThat(updated).isTrue();
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
index ceabe70333c..5a4da59435b 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
@@ -67,6 +67,7 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
private String severity = null;
private boolean manualSeverity = false;
private String message = null;
+ private Object messageFormattings = null;
private Integer line = null;
private Double gap = null;
private Duration effort = null;
@@ -261,10 +262,21 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
}
public DefaultIssue setMessage(@Nullable String s) {
+ //TODO trim messageFormattings?
this.message = StringUtils.abbreviate(StringUtils.trim(s), MESSAGE_MAX_SIZE);
return this;
}
+ @CheckForNull
+ public <T> T getMessageFormattings() {
+ return (T) messageFormattings;
+ }
+
+ public DefaultIssue setMessageFormattings(@Nullable Object messageFormattings) {
+ this.messageFormattings = messageFormattings;
+ return this;
+ }
+
@Override
@CheckForNull
public Integer line() {
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java
index cc77d90a02f..186ad5080b8 100644
--- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/AbstractDefaultIssue.java
@@ -25,6 +25,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.sonar.api.batch.fs.InputComponent;
import org.sonar.api.batch.fs.internal.DefaultInputDir;
@@ -34,8 +35,10 @@ import org.sonar.api.batch.sensor.internal.DefaultStorable;
import org.sonar.api.batch.sensor.internal.SensorStorage;
import org.sonar.api.batch.sensor.issue.Issue.Flow;
import org.sonar.api.batch.sensor.issue.IssueLocation;
+import org.sonar.api.batch.sensor.issue.MessageFormatting;
import org.sonar.api.batch.sensor.issue.NewIssue.FlowType;
import org.sonar.api.batch.sensor.issue.NewIssueLocation;
+import org.sonar.api.batch.sensor.issue.NewMessageFormatting;
import org.sonar.api.utils.PathUtils;
import static org.sonar.api.utils.Preconditions.checkArgument;
@@ -107,14 +110,31 @@ public abstract class AbstractDefaultIssue<T extends AbstractDefaultIssue> exten
DefaultIssueLocation fixedLocation = new DefaultIssueLocation();
fixedLocation.on(project);
StringBuilder fullMessage = new StringBuilder();
+ String prefixMessage;
if (path != null && !path.isEmpty()) {
- fullMessage.append("[").append(path).append("] ");
+ prefixMessage = "[" + path + "] ";
+ } else {
+ prefixMessage = "";
}
+
+ fullMessage.append(prefixMessage);
fullMessage.append(location.message());
- fixedLocation.message(fullMessage.toString());
+
+ List<NewMessageFormatting> paddedFormattings = location.messageFormattings().stream()
+ .map(m -> padMessageFormatting(m, prefixMessage.length()))
+ .collect(Collectors.toList());
+
+ fixedLocation.message(fullMessage.toString(), paddedFormattings);
+
return fixedLocation;
} else {
return location;
}
}
+
+ private static NewMessageFormatting padMessageFormatting(MessageFormatting messageFormatting, int length) {
+ return new DefaultMessageFormatting().type(messageFormatting.type())
+ .start(messageFormatting.start() + length)
+ .end(messageFormatting.end() + length);
+ }
}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java
index 4f2598d6683..ed55c0d09af 100644
--- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssue.java
@@ -19,6 +19,7 @@
*/
package org.sonar.api.batch.sensor.issue.internal;
+import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
import org.sonar.api.batch.fs.internal.DefaultInputProject;
@@ -27,6 +28,8 @@ 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.fix.NewQuickFix;
+import org.sonar.api.batch.sensor.issue.fix.QuickFix;
import org.sonar.api.rule.RuleKey;
import static java.lang.String.format;
@@ -78,6 +81,16 @@ public class DefaultIssue extends AbstractDefaultIssue<DefaultIssue> implements
}
@Override
+ public NewQuickFix newQuickFix() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public NewIssue addQuickFix(NewQuickFix newQuickFix) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
public DefaultIssue setRuleDescriptionContextKey(@Nullable String ruleDescriptionContextKey) {
this.ruleDescriptionContextKey = ruleDescriptionContextKey;
return this;
@@ -94,6 +107,11 @@ public class DefaultIssue extends AbstractDefaultIssue<DefaultIssue> implements
}
@Override
+ public List<QuickFix> quickFixes() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
public Severity overriddenSeverity() {
return this.overriddenSeverity;
}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssueLocation.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssueLocation.java
index 19160b6757b..89f1c9550f3 100644
--- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssueLocation.java
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssueLocation.java
@@ -19,12 +19,16 @@
*/
package org.sonar.api.batch.sensor.issue.internal;
+import java.util.ArrayList;
+import java.util.List;
import javax.annotation.Nullable;
import org.sonar.api.batch.fs.InputComponent;
import org.sonar.api.batch.fs.TextRange;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
import org.sonar.api.batch.sensor.issue.IssueLocation;
+import org.sonar.api.batch.sensor.issue.MessageFormatting;
import org.sonar.api.batch.sensor.issue.NewIssueLocation;
-import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.sensor.issue.NewMessageFormatting;
import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang.StringUtils.abbreviate;
@@ -37,6 +41,7 @@ public class DefaultIssueLocation implements NewIssueLocation, IssueLocation {
private InputComponent component;
private TextRange textRange;
private String message;
+ private final List<MessageFormatting> messageFormattings = new ArrayList<>();
@Override
public DefaultIssueLocation on(InputComponent component) {
@@ -58,12 +63,40 @@ public class DefaultIssueLocation implements NewIssueLocation, IssueLocation {
@Override
public DefaultIssueLocation message(String message) {
+ validateMessage(message);
+ this.message = abbreviate(trim(message), MESSAGE_MAX_SIZE);
+ return this;
+ }
+
+ @Override
+ public NewIssueLocation message(String message, List<NewMessageFormatting> newMessageFormattings) {
+ validateMessage(message);
+ validateFormattings(newMessageFormattings, message);
+ this.message = abbreviate(trim(message), MESSAGE_MAX_SIZE);
+
+ for (NewMessageFormatting newMessageFormatting : newMessageFormattings) {
+ messageFormattings.add((MessageFormatting) newMessageFormatting);
+ }
+ return this;
+ }
+
+ private static void validateFormattings(List<NewMessageFormatting> newMessageFormattings, String message) {
+ checkArgument(newMessageFormattings != null, "messageFormattings can't be null");
+ newMessageFormattings.stream()
+ .map(DefaultMessageFormatting.class::cast)
+ .forEach(e -> e.validate(message));
+ }
+
+ private void validateMessage(String message) {
requireNonNull(message, "Message can't be null");
if (message.contains("\u0000")) {
throw new IllegalArgumentException(unsupportedCharacterError(message, component));
}
- this.message = abbreviate(trim(message), MESSAGE_MAX_SIZE);
- return this;
+ }
+
+ @Override
+ public NewMessageFormatting newMessageFormatting() {
+ return new DefaultMessageFormatting();
}
private static String unsupportedCharacterError(String message, @Nullable InputComponent component) {
@@ -89,4 +122,9 @@ public class DefaultIssueLocation implements NewIssueLocation, IssueLocation {
return this.message;
}
+ @Override
+ public List<MessageFormatting> messageFormattings() {
+ return this.messageFormattings;
+ }
+
}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultMessageFormatting.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultMessageFormatting.java
new file mode 100644
index 00000000000..cf58777088e
--- /dev/null
+++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultMessageFormatting.java
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.sonar.api.batch.sensor.issue.MessageFormatting;
+import org.sonar.api.batch.sensor.issue.NewMessageFormatting;
+
+import static org.sonar.api.utils.Preconditions.checkArgument;
+
+public class DefaultMessageFormatting implements MessageFormatting, NewMessageFormatting {
+ private int start;
+ private int end;
+ private Type type;
+
+ @Override
+ public int start() {
+ return start;
+ }
+
+ @Override
+ public int end() {
+ return end;
+ }
+
+ @Override
+ public Type type() {
+ return type;
+ }
+
+ @Override
+ public DefaultMessageFormatting start(int start) {
+ this.start = start;
+ return this;
+ }
+
+ @Override
+ public DefaultMessageFormatting end(int end) {
+ this.end = end;
+ return this;
+ }
+
+ @Override
+ public DefaultMessageFormatting type(Type type) {
+ this.type = type;
+ return this;
+ }
+
+ public void validate(String message) {
+ checkArgument(this.type() != null, "Message formatting type can't be null");
+ checkArgument(this.start() >= 0, "Message formatting start must be greater or equals to 0");
+ checkArgument(this.end() <= message.length(), "Message formatting end must be lesser or equal than message size");
+ checkArgument(this.end() > this.start(), "Message formatting end must be greater than start");
+ }
+}
diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssueTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssueTest.java
index 1f874295e48..6e40c170065 100644
--- a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssueTest.java
+++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultIssueTest.java
@@ -37,10 +37,10 @@ import org.sonar.api.batch.fs.internal.DefaultTextRange;
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.batch.sensor.issue.Issue;
import org.sonar.api.batch.sensor.issue.Issue.Flow;
-import org.sonar.api.batch.sensor.issue.NewIssue;
+import org.sonar.api.batch.sensor.issue.MessageFormatting;
import org.sonar.api.batch.sensor.issue.NewIssue.FlowType;
+import org.sonar.api.batch.sensor.issue.fix.NewQuickFix;
import org.sonar.api.rule.RuleKey;
import static org.assertj.core.api.Assertions.assertThat;
@@ -60,6 +60,8 @@ public class DefaultIssueTest {
.build();
private DefaultInputProject project;
+ private final NewQuickFix quickFix = mock(NewQuickFix.class);
+
@Before
public void prepare() throws IOException {
project = new DefaultInputProject(ProjectDefinition.create()
@@ -164,16 +166,19 @@ public class DefaultIssueTest {
DefaultIssue issue = new DefaultIssue(project, storage)
.at(new DefaultIssueLocation()
.on(subModule)
- .message("Wrong way!"))
+ .message("Wrong way! with code snippet", List.of(new DefaultMessageFormatting().start(16).end(27).type(MessageFormatting.Type.CODE))))
.forRule(RULE_KEY)
.overrideSeverity(Severity.BLOCKER);
assertThat(issue.primaryLocation().inputComponent()).isEqualTo(project);
assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("repo", "rule"));
assertThat(issue.primaryLocation().textRange()).isNull();
- assertThat(issue.primaryLocation().message()).isEqualTo("[bar] Wrong way!");
+ assertThat(issue.primaryLocation().message()).isEqualTo("[bar] Wrong way! with code snippet");
assertThat(issue.overriddenSeverity()).isEqualTo(Severity.BLOCKER);
-
+ assertThat(issue.primaryLocation().messageFormattings().get(0)).extracting(MessageFormatting::start,
+ MessageFormatting::end, MessageFormatting::type)
+ .as("Formatting ranges are padded with the new message")
+ .containsExactly(22, 33, MessageFormatting.Type.CODE);
issue.save();
verify(storage).store(issue);
@@ -227,4 +232,14 @@ public class DefaultIssueTest {
assertThat(issue.isQuickFixAvailable()).isTrue();
}
+ @Test
+ public void quickfix_not_supported_for_now() {
+ DefaultIssue issue = new DefaultIssue(project);
+ assertThatThrownBy(() -> issue.addQuickFix(quickFix))
+ .isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(issue::newQuickFix)
+ .isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(issue::quickFixes)
+ .isInstanceOf(UnsupportedOperationException.class);
+ }
}
diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultMessageFormattingTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultMessageFormattingTest.java
new file mode 100644
index 00000000000..55a67ddd485
--- /dev/null
+++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultMessageFormattingTest.java
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.assertj.core.api.Assertions;
+import org.junit.Test;
+import org.sonar.api.batch.sensor.issue.MessageFormatting;
+
+public class DefaultMessageFormattingTest {
+
+ @Test
+ public void negative_start_should_throw_exception() {
+ DefaultMessageFormatting format = new DefaultMessageFormatting().start(-1).end(1).type(MessageFormatting.Type.CODE);
+ Assertions.assertThatThrownBy(() -> format.validate("message"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Message formatting start must be greater or equals to 0");
+ }
+
+ @Test
+ public void missing_type_should_throw_exception() {
+ DefaultMessageFormatting format = new DefaultMessageFormatting().start(0).end(1);
+ Assertions.assertThatThrownBy(() -> format.validate("message"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Message formatting type can't be null");
+ }
+
+ @Test
+ public void end_lesser_than_start_should_throw_exception() {
+ DefaultMessageFormatting format = new DefaultMessageFormatting().start(3).end(2).type(MessageFormatting.Type.CODE);
+ Assertions.assertThatThrownBy(() -> format.validate("message"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Message formatting end must be greater than start");
+ }
+
+ @Test
+ public void end_greater_or_equals_to_message_size_throw_exception() {
+ DefaultMessageFormatting format = new DefaultMessageFormatting().start(0).end(8).type(MessageFormatting.Type.CODE);
+ Assertions.assertThatThrownBy(() -> format.validate("message"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Message formatting end must be lesser or equal than message size");
+ }
+
+ @Test
+ public void full_range_on_message_should_work() {
+ DefaultMessageFormatting format = new DefaultMessageFormatting().start(0).end(6).type(MessageFormatting.Type.CODE);
+ Assertions.assertThatCode(() -> format.validate("message")).doesNotThrowAnyException();
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuePublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuePublisher.java
index 8688c6a3ebe..d3a26a55d95 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuePublisher.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuePublisher.java
@@ -20,7 +20,9 @@
package org.sonar.scanner.issue;
import java.util.Collection;
+import java.util.List;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import org.apache.commons.lang.StringUtils;
@@ -32,6 +34,7 @@ import org.sonar.api.batch.rule.ActiveRules;
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.batch.sensor.issue.MessageFormatting;
import org.sonar.api.batch.sensor.issue.NewIssue.FlowType;
import org.sonar.api.batch.sensor.issue.internal.DefaultIssueFlow;
import org.sonar.scanner.protocol.Constants.Severity;
@@ -112,7 +115,9 @@ public class IssuePublisher {
builder.setRuleRepository(issue.ruleKey().repository());
builder.setRuleKey(issue.ruleKey().rule());
builder.setMsg(primaryMessage);
+ builder.addAllMsgFormatting(toProtobufMessageFormattings(issue.primaryLocation().messageFormattings()));
locationBuilder.setMsg(primaryMessage);
+ locationBuilder.addAllMsgFormatting(toProtobufMessageFormattings(issue.primaryLocation().messageFormattings()));
locationBuilder.setComponentRef(componentRef);
TextRange primaryTextRange = issue.primaryLocation().textRange();
@@ -129,6 +134,16 @@ public class IssuePublisher {
return builder.build();
}
+ private static List<ScannerReport.MessageFormatting> toProtobufMessageFormattings(List<MessageFormatting> messageFormattings) {
+ return messageFormattings.stream()
+ .map(m -> ScannerReport.MessageFormatting.newBuilder()
+ .setStart(m.start())
+ .setEnd(m.end())
+ .setType(ScannerReport.MessageFormattingType.valueOf(m.type().name()))
+ .build())
+ .collect(Collectors.toList());
+ }
+
private static ScannerReport.ExternalIssue createReportExternalIssue(ExternalIssue issue, int componentRef) {
// primary location of an external issue must have a message
String primaryMessage = issue.primaryLocation().message();
@@ -144,8 +159,9 @@ public class IssuePublisher {
builder.setEngineId(issue.engineId());
builder.setRuleId(issue.ruleId());
builder.setMsg(primaryMessage);
+ builder.addAllMsgFormatting(toProtobufMessageFormattings(issue.primaryLocation().messageFormattings()));
locationBuilder.setMsg(primaryMessage);
-
+ locationBuilder.addAllMsgFormatting(toProtobufMessageFormattings(issue.primaryLocation().messageFormattings()));
locationBuilder.setComponentRef(componentRef);
TextRange primaryTextRange = issue.primaryLocation().textRange();
if (primaryTextRange != null) {
@@ -175,6 +191,7 @@ public class IssuePublisher {
String message = location.message();
if (message != null) {
locationBuilder.setMsg(message);
+ locationBuilder.addAllMsgFormatting(toProtobufMessageFormattings(location.messageFormattings()));
}
TextRange textRange = location.textRange();
if (textRange != null) {
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuePublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuePublisherTest.java
index becb9a339f1..ada4b74e15b 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuePublisherTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuePublisherTest.java
@@ -41,6 +41,7 @@ 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.issue.internal.DefaultIssueLocation;
+import org.sonar.api.batch.sensor.issue.internal.DefaultMessageFormatting;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.Severity;
import org.sonar.api.rules.RuleType;
@@ -57,6 +58,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
+import static org.sonar.api.batch.sensor.issue.MessageFormatting.Type.CODE;
@RunWith(MockitoJUnitRunner.class)
public class IssuePublisherTest {
@@ -143,10 +145,12 @@ public class IssuePublisherTest {
public void add_issue_flows_to_cache() {
initModuleIssues();
+ DefaultMessageFormatting messageFormatting = new DefaultMessageFormatting().start(0).end(4).type(CODE);
DefaultIssue issue = new DefaultIssue(project)
.at(new DefaultIssueLocation().on(file))
// Flow without type
- .addFlow(List.of(new DefaultIssueLocation().on(file).at(file.selectLine(1)).message("Foo1"), new DefaultIssueLocation().on(file).at(file.selectLine(2)).message("Foo2")))
+ .addFlow(List.of(new DefaultIssueLocation().on(file).at(file.selectLine(1)).message("Foo1", List.of(messageFormatting)),
+ new DefaultIssueLocation().on(file).at(file.selectLine(2)).message("Foo2")))
// Flow with type and description
.addFlow(List.of(new DefaultIssueLocation().on(file)), NewIssue.FlowType.DATA, "description")
// Flow with execution type and no description
@@ -169,6 +173,7 @@ public class IssuePublisherTest {
ScannerReport.IssueLocation.newBuilder()
.setComponentRef(file.scannerId())
.setMsg("Foo1")
+ .addMsgFormatting(ScannerReport.MessageFormatting.newBuilder().setStart(0).setEnd(4).setType(ScannerReport.MessageFormattingType.CODE).build())
.setTextRange(ScannerReport.TextRange.newBuilder().setStartLine(1).setEndLine(1).setEndOffset(3).build())
.build(),
ScannerReport.IssueLocation.newBuilder()
diff --git a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto
index bcb9f6e5d52..beb59561354 100644
--- a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto
+++ b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto
@@ -198,6 +198,7 @@ message Issue {
repeated Flow flow = 7;
bool quickFixAvailable = 8;
optional string ruleDescriptionContextKey = 9;
+ repeated MessageFormatting msgFormatting = 10;
}
message ExternalIssue {
@@ -209,6 +210,7 @@ message ExternalIssue {
TextRange text_range = 6;
repeated Flow flow = 7;
IssueType type = 8;
+ repeated MessageFormatting msgFormatting = 9;
}
message AdHocRule {
@@ -233,6 +235,17 @@ message IssueLocation {
// Only when component is a file. Can be empty for a file if this is an issue global to the file.
TextRange text_range = 2;
string msg = 3;
+ repeated MessageFormatting msgFormatting = 4;
+}
+
+message MessageFormatting {
+ int32 start = 1;
+ int32 end = 2;
+ MessageFormattingType type = 3;
+}
+
+enum MessageFormattingType {
+ CODE = 0;
}
message Flow {