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;
+import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.ce.task.projectanalysis.batch.BatchReportReader;
import org.sonar.ce.task.projectanalysis.component.Component;
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;
import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
public class TrackerRawInputFactory {
+ private static final Logger LOGGER = Loggers.get(TrackerRawInputFactory.class);
+ 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;
}
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, component));
}
for (ScannerReport.Flow flow : reportIssue.getFlowList()) {
if (flow.getLocationCount() > 0) {
ScannerReport.TextRange sourceRange = source.getTextRange();
DbCommons.TextRange.Builder targetRange = convertTextRange(sourceRange);
target.setTextRange(targetRange);
+ target.setChecksum(calculateLocationHash(targetRange, source.getComponentRef()));
}
return Optional.of(target.build());
}
+ private String calculateLocationHash(DbCommons.TextRange.Builder textRange, int componentRef) {
+ if (this.component.getReportAttributes().getRef() == null) {
+ LOGGER.warn("Line hash for one of the issues in component" + component.getName() + " will not be calculated");
+ return "";
+ }
+ if (componentRef == this.component.getReportAttributes().getRef()) {
+ return calculateLocationHash(textRange, this.component);
+ }
+ Component textRangeComponent = treeRootHolder.getComponentByRef(componentRef);
+ return calculateLocationHash(textRange, textRangeComponent);
+ }
+
+ private String calculateLocationHash(DbCommons.TextRange.Builder textRange, Component component) {
+ try (CloseableIterator<String> linesIterator = sourceLinesRepository.readLines(component)) {
+ StringBuilder toHash = new StringBuilder();
+ int lineNumber = 1;
+ while (linesIterator.hasNext()) {
+ String line = linesIterator.next();
+ if (lineNumber == textRange.getStartLine() && lineNumber == textRange.getEndLine()) {
+ toHash.append(line, textRange.getStartOffset(), textRange.getEndOffset());
+ } else if (lineNumber == textRange.getStartLine()) {
+ toHash.append(line, textRange.getStartOffset(), line.length());
+ } else if (lineNumber > textRange.getStartLine() && lineNumber < textRange.getEndLine()) {
+ toHash.append(line);
+ } else if (lineNumber == textRange.getEndLine()) {
+ toHash.append(line, 0, textRange.getEndOffset());
+ } else if (lineNumber > textRange.getEndLine()) {
+ break;
+ }
+ lineNumber++;
+ }
+ 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());
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.Collection;
import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+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;
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;
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 String LINE_IN_THE_MAIN_FILE = "String string = 'line-in-the-main-file';";
+ private static final String LINE_IN_ANOTHER_FILE = "String string = 'line-in-the-another-file';";
+ 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);
@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() {
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())
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
+ public void calculateLocationHash_givenIssueOn2Components_calculateHashesByReading2Files() {
+ when(sourceLinesRepository.readLines(any())).thenReturn(
+ newOneLineIterator(LINE_IN_THE_MAIN_FILE),
+ newOneLineIterator(LINE_IN_THE_MAIN_FILE),
+ newOneLineIterator(LINE_IN_ANOTHER_FILE));
+ RuleKey ruleKey = RuleKey.of("java", "S001");
+ markRuleAsActive(ruleKey);
+ when(issueFilter.accept(any(), eq(FILE))).thenReturn(true);
+
+ ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
+ .setTextRange(newTextRange(1, LINE_IN_THE_MAIN_FILE.length()))
+ .setMsg("the message")
+ .setRuleRepository(ruleKey.repository())
+ .setRuleKey(ruleKey.rule())
+ .setSeverity(Constants.Severity.BLOCKER)
+ .setGap(3.14)
+ .addFlow(ScannerReport.Flow.newBuilder()
+ .addLocation(ScannerReport.IssueLocation.newBuilder()
+ .setComponentRef(FILE_REF)
+ .setMsg("Secondary location in same file")
+ .setTextRange(newTextRange(1, LINE_IN_THE_MAIN_FILE.length())))
+ .addLocation(ScannerReport.IssueLocation.newBuilder()
+ .setComponentRef(ANOTHER_FILE_REF)
+ .setMsg("Secondary location in other file")
+ .setTextRange(newTextRange(1, LINE_IN_ANOTHER_FILE.length())))
+ .build())
+ .build();
+ reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue));
+
+ Input<DefaultIssue> input = underTest.create(FILE);
+ DefaultIssue issue = Iterators.getOnlyElement(input.getIssues().iterator());
+
+ DbIssues.Locations locations = issue.getLocations();
+
+ assertThat(locations.getFlow(0).getLocation(0).getChecksum()).isEqualTo(DigestUtils.md5Hex("Stringstring='line-in-the-main-file';"));
+ assertThat(locations.getFlow(0).getLocation(1).getChecksum()).isEqualTo(DigestUtils.md5Hex("Stringstring='line-in-the-another-file';"));
+ }
+
+ private CloseableIterator<String> newOneLineIterator(String lineContent) {
+ return CloseableIterator.from(List.of(lineContent).iterator());
}
@Test
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())
.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));
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")
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")
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())
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())
ruleRepository.add(dumbRule);
}
+ private TextRange newTextRange(int issueOnLine, int endOffset) {
+ return TextRange.newBuilder()
+ .setStartLine(issueOnLine)
+ .setEndLine(issueOnLine)
+ .setStartOffset(0)
+ .setEndOffset(endOffset)
+ .build();
+ }
+
+ 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);
+ }
}