From 66b8bff0dcc778eb93da92cd408214bbfca82c2a Mon Sep 17 00:00:00 2001 From: =?utf8?q?L=C3=A9o=20Geoffroy?= Date: Wed, 23 Nov 2022 15:57:02 +0100 Subject: [PATCH] SONAR-17592 Persist Message formatting from plugin api --- build.gradle | 2 +- .../projectanalysis/issue/IssueLifecycle.java | 5 +- .../issue/TrackerRawInputFactory.java | 26 +++++++ .../util/cache/ProtobufIssueDiskCache.java | 2 + .../src/main/protobuf/issue_cache.proto | 1 + .../issue/IssueLifecycleTest.java | 15 +++- .../issue/TrackerRawInputFactoryTest.java | 28 +++++++- .../java/org/sonar/db/issue/IssueDto.java | 23 ++++-- .../src/main/protobuf/db-issues.proto | 2 +- .../org/sonar/db/issue/IssueMapper.xml | 12 +++- .../java/org/sonar/db/issue/IssueDaoTest.java | 42 +++++++---- .../java/org/sonar/db/issue/IssueDtoTest.java | 29 ++++---- .../db/migration/version/v93/DbVersion93.java | 1 + .../sonar/server/issue/IssueFieldsSetter.java | 42 ++++++++++- .../server/issue/IssueFieldsSetterTest.java | 72 ++++++++++++++++++- .../org/sonar/core/issue/DefaultIssue.java | 12 ++++ .../issue/internal/AbstractDefaultIssue.java | 24 ++++++- .../sensor/issue/internal/DefaultIssue.java | 18 +++++ .../issue/internal/DefaultIssueLocation.java | 44 +++++++++++- .../internal/DefaultMessageFormatting.java | 71 ++++++++++++++++++ .../issue/internal/DefaultIssueTest.java | 25 +++++-- .../DefaultMessageFormattingTest.java | 65 +++++++++++++++++ .../sonar/scanner/issue/IssuePublisher.java | 19 ++++- .../scanner/issue/IssuePublisherTest.java | 7 +- .../src/main/protobuf/scanner_report.proto | 13 ++++ 25 files changed, 538 insertions(+), 62 deletions(-) create mode 100644 sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/issue/internal/DefaultMessageFormatting.java create mode 100644 sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/issue/internal/DefaultMessageFormattingTest.java 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 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.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 { 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 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 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 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"); @@ -509,6 +512,73 @@ public class IssueFieldsSetterTest { assertThat(issue.mustSendNotifications()).isFalse(); } + @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); 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 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 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 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; @@ -77,6 +80,16 @@ public class DefaultIssue extends AbstractDefaultIssue implements return this; } + @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; @@ -93,6 +106,11 @@ public class DefaultIssue extends AbstractDefaultIssue implements return Optional.ofNullable(ruleDescriptionContextKey); } + @Override + public List 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 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 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 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 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 toProtobufMessageFormattings(List 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 { -- 2.39.5