]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16370 compute issue hashes when processing scanner report
authorLukasz Jarocki <lukasz.jarocki@sonarsource.com>
Wed, 11 May 2022 07:44:57 +0000 (09:44 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 11 May 2022 20:02:59 +0000 (20:02 +0000)
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java
server/sonar-db-dao/src/main/protobuf/db-issues.proto

index 988c778cce7372f7b476b00b22aa14962c75eae4..0f6ae1f28e50540d2473227f8bfa14ff09aa13a3 100644 (file)
@@ -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<String> 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());
index 986f6d1f9af183537f959b4d6ea3d78cbe12fc8c..16588a66910c770e444f450d3e46ca67d919b526 100644 (file)
@@ -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<DefaultIssue> 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));
index 2547336bd8f76ae0492a7897aa61279f471947fa..4887cc3c2aaf63ea74aa901c7b93ee6c73762f57 100644 (file)
@@ -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<String> 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<DefaultIssue> 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<DefaultIssue> 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<DefaultIssue> 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<DefaultIssue> 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);
+  }
 
 }
index 1981d89a422f3958757ab50630869ca17d78b595..af997adac180b81a1d556d7311285299b2884472 100644 (file)
@@ -67,5 +67,4 @@ public interface IssueMapper {
   Collection<IssueGroupDto> selectIssueGroupsByBaseComponent(
     @Param("baseComponent") ComponentDto baseComponent,
     @Param("leakPeriodBeginningDate") long leakPeriodBeginningDate);
-
 }
index c57edc7b0e7f6bce518d24030553860436ca0225..6b64ce30f5955a5091f951b178324a8fd46f874e 100644 (file)
@@ -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;
 }