From 7cb2cbffa171aa07b79f92a0e2d3ac3252641f3a Mon Sep 17 00:00:00 2001 From: Lukasz Jarocki Date: Wed, 11 May 2022 09:44:57 +0200 Subject: [PATCH] SONAR-16370 compute issue hashes when processing scanner report --- .../issue/TrackerRawInputFactory.java | 40 ++++- .../issue/IntegrateIssuesVisitorTest.java | 4 +- .../issue/TrackerRawInputFactoryTest.java | 148 +++++++++++++++--- .../java/org/sonar/db/issue/IssueMapper.java | 1 - .../src/main/protobuf/db-issues.proto | 2 + 5 files changed, 169 insertions(+), 26 deletions(-) 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 988c778cce7..0f6ae1f28e5 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 @@ -25,7 +25,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.regex.Pattern; import javax.annotation.Nullable; +import org.apache.commons.codec.digest.DigestUtils; import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.Duration; @@ -37,6 +39,7 @@ import org.sonar.ce.task.projectanalysis.issue.commonrule.CommonRuleEngine; import org.sonar.ce.task.projectanalysis.issue.filter.IssueFilter; import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolder; import org.sonar.ce.task.projectanalysis.source.SourceLinesHashRepository; +import org.sonar.ce.task.projectanalysis.source.SourceLinesRepository; import org.sonar.core.issue.DefaultIssue; import org.sonar.core.issue.tracking.Input; import org.sonar.core.issue.tracking.LazyInput; @@ -54,21 +57,24 @@ import static org.sonar.api.issue.Issue.STATUS_OPEN; import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW; public class TrackerRawInputFactory { + private static final Pattern MATCH_ALL_WHITESPACES = Pattern.compile("\\s"); private static final long DEFAULT_EXTERNAL_ISSUE_EFFORT = 0L; private final TreeRootHolder treeRootHolder; private final BatchReportReader reportReader; private final CommonRuleEngine commonRuleEngine; private final IssueFilter issueFilter; private final SourceLinesHashRepository sourceLinesHash; + private final SourceLinesRepository sourceLinesRepository; private final RuleRepository ruleRepository; private final ActiveRulesHolder activeRulesHolder; - public TrackerRawInputFactory(TreeRootHolder treeRootHolder, BatchReportReader reportReader, - SourceLinesHashRepository sourceLinesHash, CommonRuleEngine commonRuleEngine, IssueFilter issueFilter, RuleRepository ruleRepository, + public TrackerRawInputFactory(TreeRootHolder treeRootHolder, BatchReportReader reportReader, SourceLinesHashRepository sourceLinesHash, + SourceLinesRepository sourceLinesRepository, CommonRuleEngine commonRuleEngine, IssueFilter issueFilter, RuleRepository ruleRepository, ActiveRulesHolder activeRulesHolder) { this.treeRootHolder = treeRootHolder; this.reportReader = reportReader; this.sourceLinesHash = sourceLinesHash; + this.sourceLinesRepository = sourceLinesRepository; this.commonRuleEngine = commonRuleEngine; this.issueFilter = issueFilter; this.ruleRepository = ruleRepository; @@ -185,7 +191,9 @@ public class TrackerRawInputFactory { } DbIssues.Locations.Builder dbLocationsBuilder = DbIssues.Locations.newBuilder(); if (reportIssue.hasTextRange()) { - dbLocationsBuilder.setTextRange(convertTextRange(reportIssue.getTextRange())); + DbCommons.TextRange.Builder textRange = convertTextRange(reportIssue.getTextRange()); + dbLocationsBuilder.setTextRange(textRange); + dbLocationsBuilder.setChecksum(calculateLocationHash(textRange)); } for (ScannerReport.Flow flow : reportIssue.getFlowList()) { if (flow.getLocationCount() > 0) { @@ -294,10 +302,36 @@ public class TrackerRawInputFactory { ScannerReport.TextRange sourceRange = source.getTextRange(); DbCommons.TextRange.Builder targetRange = convertTextRange(sourceRange); target.setTextRange(targetRange); + target.setChecksum(calculateLocationHash(targetRange)); } return Optional.of(target.build()); } + private String calculateLocationHash(DbCommons.TextRange.Builder textRange) { + try (CloseableIterator linesIterator = sourceLinesRepository.readLines(component)) { + StringBuilder toHash = new StringBuilder(); + int lineNo = 1; + while (linesIterator.hasNext()) { + String line = linesIterator.next(); + if (lineNo == textRange.getStartLine() && lineNo == textRange.getEndLine()) { + toHash.append(line, textRange.getStartOffset(), textRange.getEndOffset()); + } else if (lineNo == textRange.getStartLine()) { + toHash.append(line, textRange.getStartOffset(), line.length()); + } else if (lineNo > textRange.getStartLine() && lineNo < textRange.getEndLine()) { + toHash.append(line); + } else if (lineNo == textRange.getEndLine()) { + toHash.append(line, 0, textRange.getEndOffset()); + } else if (lineNo > textRange.getEndLine()) { + break; + } + lineNo++; + } + String issueContentWithoutWhitespaces = MATCH_ALL_WHITESPACES.matcher(toHash.toString()).replaceAll(""); + return DigestUtils.md5Hex(issueContentWithoutWhitespaces); + } + + } + private DbCommons.TextRange.Builder convertTextRange(ScannerReport.TextRange sourceRange) { DbCommons.TextRange.Builder targetRange = DbCommons.TextRange.newBuilder(); targetRange.setStartLine(sourceRange.getStartLine()); diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java index 986f6d1f9af..16588a66910 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java @@ -49,6 +49,7 @@ import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolderRule; import org.sonar.ce.task.projectanalysis.qualityprofile.AlwaysActiveRulesHolderImpl; import org.sonar.ce.task.projectanalysis.source.NewLinesRepository; import org.sonar.ce.task.projectanalysis.source.SourceLinesHashRepository; +import org.sonar.ce.task.projectanalysis.source.SourceLinesRepository; import org.sonar.ce.task.projectanalysis.source.SourceLinesRepositoryRule; import org.sonar.core.issue.DefaultIssue; import org.sonar.core.issue.FieldDiffs; @@ -126,6 +127,7 @@ public class IntegrateIssuesVisitorTest { private final SiblingsIssueMerger issueStatusCopier = mock(SiblingsIssueMerger.class); private final ReferenceBranchComponentUuids referenceBranchComponentUuids = mock(ReferenceBranchComponentUuids.class); private final SourceLinesHashRepository sourceLinesHash = mock(SourceLinesHashRepository.class); + private final SourceLinesRepository sourceLinesRepository = mock(SourceLinesRepository.class); private final NewLinesRepository newLinesRepository = mock(NewLinesRepository.class); private TargetBranchComponentUuids targetBranchComponentUuids = mock(TargetBranchComponentUuids.class); private ArgumentCaptor defaultIssueCaptor; @@ -149,7 +151,7 @@ public class IntegrateIssuesVisitorTest { when(movedFilesRepository.getOriginalFile(any(Component.class))).thenReturn(Optional.empty()); DbClient dbClient = dbTester.getDbClient(); - TrackerRawInputFactory rawInputFactory = new TrackerRawInputFactory(treeRootHolder, reportReader, sourceLinesHash, new CommonRuleEngineImpl(), + TrackerRawInputFactory rawInputFactory = new TrackerRawInputFactory(treeRootHolder, reportReader, sourceLinesHash, sourceLinesRepository, new CommonRuleEngineImpl(), issueFilter, ruleRepositoryRule, activeRulesHolder); TrackerBaseInputFactory baseInputFactory = new TrackerBaseInputFactory(issuesLoader, dbClient, movedFilesRepository, mock(ReportModulesPath.class), analysisMetadataHolder, new IssueFieldsSetter(), mock(ComponentsWithUnprocessedIssues.class)); 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 2547336bd8f..4887cc3c2aa 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 @@ -25,6 +25,10 @@ import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; +import java.util.stream.IntStream; +import org.apache.commons.codec.digest.DigestUtils; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -41,8 +45,10 @@ import org.sonar.ce.task.projectanalysis.issue.filter.IssueFilter; import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRule; import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolderRule; import org.sonar.ce.task.projectanalysis.source.SourceLinesHashRepository; +import org.sonar.ce.task.projectanalysis.source.SourceLinesRepository; import org.sonar.core.issue.DefaultIssue; import org.sonar.core.issue.tracking.Input; +import org.sonar.core.util.CloseableIterator; import org.sonar.db.protobuf.DbIssues; import org.sonar.scanner.protocol.Constants; import org.sonar.scanner.protocol.output.ScannerReport; @@ -66,13 +72,10 @@ public class TrackerRawInputFactoryTest { private static final String FILE_UUID = "fake_uuid"; private static final String ANOTHER_FILE_UUID = "another_fake_uuid"; - private static int FILE_REF = 2; - private static int NOT_IN_REPORT_FILE_REF = 3; - private static int ANOTHER_FILE_REF = 4; - - private static ReportComponent FILE = ReportComponent.builder(Component.Type.FILE, FILE_REF).setUuid(FILE_UUID).build(); - private static ReportComponent ANOTHER_FILE = ReportComponent.builder(Component.Type.FILE, ANOTHER_FILE_REF).setUuid(ANOTHER_FILE_UUID).build(); - private static ReportComponent PROJECT = ReportComponent.builder(Component.Type.PROJECT, 1).addChildren(FILE, ANOTHER_FILE).build(); + private static final String EXAMPLE_LINE_OF_CODE_FORMAT = "int example = line + of + code + %d; "; + private static final int FILE_REF = 2; + private static final int NOT_IN_REPORT_FILE_REF = 3; + private static final int ANOTHER_FILE_REF = 4; @Rule public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule().setRoot(PROJECT); @@ -83,11 +86,24 @@ public class TrackerRawInputFactoryTest { @Rule public RuleRepositoryRule ruleRepository = new RuleRepositoryRule(); - private SourceLinesHashRepository sourceLinesHash = mock(SourceLinesHashRepository.class); - private CommonRuleEngine commonRuleEngine = mock(CommonRuleEngine.class); - private IssueFilter issueFilter = mock(IssueFilter.class); - private TrackerRawInputFactory underTest = new TrackerRawInputFactory(treeRootHolder, reportReader, sourceLinesHash, - commonRuleEngine, issueFilter, ruleRepository, activeRulesHolder); + private static final ReportComponent FILE = ReportComponent.builder(Component.Type.FILE, FILE_REF).setUuid(FILE_UUID).build(); + private static final ReportComponent ANOTHER_FILE = ReportComponent.builder(Component.Type.FILE, ANOTHER_FILE_REF).setUuid(ANOTHER_FILE_UUID).build(); + private static final ReportComponent PROJECT = ReportComponent.builder(Component.Type.PROJECT, 1).addChildren(FILE, ANOTHER_FILE).build(); + + private final SourceLinesHashRepository sourceLinesHash = mock(SourceLinesHashRepository.class); + private final SourceLinesRepository sourceLinesRepository = mock(SourceLinesRepository.class); + private final CommonRuleEngine commonRuleEngine = mock(CommonRuleEngine.class); + private final IssueFilter issueFilter = mock(IssueFilter.class); + private final TrackerRawInputFactory underTest = new TrackerRawInputFactory(treeRootHolder, reportReader, sourceLinesHash, + sourceLinesRepository, commonRuleEngine, issueFilter, ruleRepository, activeRulesHolder); + + @Before + public void before() { + Iterator stringIterator = IntStream.rangeClosed(1, 9) + .mapToObj(i -> String.format(EXAMPLE_LINE_OF_CODE_FORMAT, i)) + .iterator(); + when(sourceLinesRepository.readLines(any())).thenReturn(CloseableIterator.from(stringIterator)); + } @Test public void load_source_hash_sequences() { @@ -118,7 +134,7 @@ public class TrackerRawInputFactoryTest { when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line")); ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder() - .setTextRange(TextRange.newBuilder().setStartLine(2).build()) + .setTextRange(newTextRange(2)) .setMsg("the message") .setRuleRepository(ruleKey.repository()) .setRuleKey(ruleKey.rule()) @@ -146,6 +162,80 @@ public class TrackerRawInputFactoryTest { assertThat(issue.tags()).isEmpty(); assertInitializedIssue(issue); assertThat(issue.effort()).isNull(); + + assertLocationHashIsMadeOf(input, "intexample=line+of+code+2;"); + } + + @Test + public void calculateLocationHash_givenIssueOn3Lines_calculateHashOn3Lines() { + RuleKey ruleKey = RuleKey.of("java", "S001"); + markRuleAsActive(ruleKey); + when(issueFilter.accept(any(), eq(FILE))).thenReturn(true); + + ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder() + .setTextRange(TextRange.newBuilder() + .setStartLine(1) + .setEndLine(3) + .setStartOffset(0) + .setEndOffset(EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1) + .build()) + .setMsg("the message") + .setRuleRepository(ruleKey.repository()) + .setRuleKey(ruleKey.rule()) + .build(); + reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue)); + + Input input = underTest.create(FILE); + + assertLocationHashIsMadeOf(input, "intexample=line+of+code+1;intexample=line+of+code+2;intexample=line+of+code+3;"); + } + + @Test + public void calculateLocationHash_givenIssuePartiallyOn1Line_calculateHashOnAPartOfLine() { + RuleKey ruleKey = RuleKey.of("java", "S001"); + markRuleAsActive(ruleKey); + when(issueFilter.accept(any(), eq(FILE))).thenReturn(true); + + ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder() + .setTextRange(TextRange.newBuilder() + .setStartLine(1) + .setEndLine(1) + .setStartOffset(13) + .setEndOffset(EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1) + .build()) + .setMsg("the message") + .setRuleRepository(ruleKey.repository()) + .setRuleKey(ruleKey.rule()) + .build(); + reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue)); + + Input input = underTest.create(FILE); + + assertLocationHashIsMadeOf(input, "line+of+code+1;"); + } + + @Test + public void calculateLocationHash_givenIssuePartiallyOn1LineAndPartiallyOnThirdLine_calculateHashAccordingly() { + RuleKey ruleKey = RuleKey.of("java", "S001"); + markRuleAsActive(ruleKey); + when(issueFilter.accept(any(), eq(FILE))).thenReturn(true); + + ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder() + .setTextRange(TextRange.newBuilder() + .setStartLine(1) + .setEndLine(3) + .setStartOffset(13) + .setEndOffset(11) + .build()) + .setMsg("the message") + .setRuleRepository(ruleKey.repository()) + .setRuleKey(ruleKey.rule()) + .build(); + reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue)); + + Input input = underTest.create(FILE); + + assertLocationHashIsMadeOf(input, "line+of+code+1;intexample=line+of+code+2;intexample"); } @Test @@ -184,7 +274,7 @@ public class TrackerRawInputFactoryTest { when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line")); ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder() - .setTextRange(TextRange.newBuilder().setStartLine(2).build()) + .setTextRange(newTextRange(2)) .setMsg("the message") .setRuleRepository(ruleKey.repository()) .setRuleKey(ruleKey.rule()) @@ -194,15 +284,15 @@ public class TrackerRawInputFactoryTest { .addLocation(ScannerReport.IssueLocation.newBuilder() .setComponentRef(FILE_REF) .setMsg("Secondary location in same file") - .setTextRange(TextRange.newBuilder().setStartLine(2).build())) + .setTextRange(newTextRange(2))) .addLocation(ScannerReport.IssueLocation.newBuilder() .setComponentRef(NOT_IN_REPORT_FILE_REF) .setMsg("Secondary location in a missing file") - .setTextRange(TextRange.newBuilder().setStartLine(3).build())) + .setTextRange(newTextRange(3))) .addLocation(ScannerReport.IssueLocation.newBuilder() .setComponentRef(ANOTHER_FILE_REF) .setMsg("Secondary location in another file") - .setTextRange(TextRange.newBuilder().setStartLine(3).build())) + .setTextRange(newTextRange(3))) .build()) .build(); reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue)); @@ -226,7 +316,7 @@ public class TrackerRawInputFactoryTest { public void load_external_issues_from_report(IssueType issueType, RuleType expectedRuleType, String expectedStatus) { when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line")); ScannerReport.ExternalIssue reportIssue = ScannerReport.ExternalIssue.newBuilder() - .setTextRange(TextRange.newBuilder().setStartLine(2).build()) + .setTextRange(newTextRange(2)) .setMsg("the message") .setEngineId("eslint") .setRuleId("S001") @@ -270,7 +360,7 @@ public class TrackerRawInputFactoryTest { public void load_external_issues_from_report_with_default_effort(IssueType issueType, RuleType expectedRuleType, String expectedStatus) { when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line")); ScannerReport.ExternalIssue reportIssue = ScannerReport.ExternalIssue.newBuilder() - .setTextRange(TextRange.newBuilder().setStartLine(2).build()) + .setTextRange(newTextRange(2)) .setMsg("the message") .setEngineId("eslint") .setRuleId("S001") @@ -305,7 +395,7 @@ public class TrackerRawInputFactoryTest { when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line")); ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder() - .setTextRange(TextRange.newBuilder().setStartLine(2).build()) + .setTextRange(newTextRange(2)) .setMsg("the message") .setRuleRepository(ruleKey.repository()) .setRuleKey(ruleKey.rule()) @@ -326,7 +416,7 @@ public class TrackerRawInputFactoryTest { when(issueFilter.accept(any(), eq(FILE))).thenReturn(false); when(sourceLinesHash.getLineHashesMatchingDBVersion(FILE)).thenReturn(Collections.singletonList("line")); ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder() - .setTextRange(TextRange.newBuilder().setStartLine(2).build()) + .setTextRange(newTextRange(2)) .setMsg("the message") .setRuleRepository(ruleKey.repository()) .setRuleKey(ruleKey.rule()) @@ -419,5 +509,21 @@ public class TrackerRawInputFactoryTest { ruleRepository.add(dumbRule); } + private TextRange newTextRange(int issueOnLine) { + return TextRange.newBuilder() + .setStartLine(issueOnLine) + .setEndLine(issueOnLine) + .setStartOffset(0) + .setEndOffset(EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1) + .build(); + } + + private void assertLocationHashIsMadeOf(Input input, String stringToHash) { + DefaultIssue defaultIssue = Iterators.getOnlyElement(input.getIssues().iterator()); + String expectedHash = DigestUtils.md5Hex(stringToHash); + DbIssues.Locations locations = defaultIssue.getLocations(); + + assertThat(locations.getChecksum()).isEqualTo(expectedHash); + } } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java index 1981d89a422..af997adac18 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java @@ -67,5 +67,4 @@ public interface IssueMapper { Collection selectIssueGroupsByBaseComponent( @Param("baseComponent") ComponentDto baseComponent, @Param("leakPeriodBeginningDate") long leakPeriodBeginningDate); - } 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 c57edc7b0e7..6b64ce30f59 100644 --- a/server/sonar-db-dao/src/main/protobuf/db-issues.proto +++ b/server/sonar-db-dao/src/main/protobuf/db-issues.proto @@ -33,6 +33,7 @@ option optimize_for = SPEED; message Locations { optional sonarqube.db.commons.TextRange text_range = 1; repeated Flow flow = 2; + optional string checksum = 3; } message Flow { @@ -44,4 +45,5 @@ message Location { // Only when component is a file. Can be empty for a file if this is an issue global to the file. optional sonarqube.db.commons.TextRange text_range = 2; optional string msg = 3; + optional string checksum = 4; } -- 2.39.5