From 6a32765903d0d2162f04172f811206ab9a78a435 Mon Sep 17 00:00:00 2001 From: Duarte Meneses Date: Tue, 26 Jul 2022 11:25:34 -0500 Subject: [PATCH] SONAR-16583 Collection of hashes of issue locations takes too long --- ...ProjectAnalysisTaskContainerPopulator.java | 2 + .../issue/ComputeLocationHashesVisitor.java | 215 ++++++++++++++ .../issue/TrackerRawInputFactory.java | 49 +--- .../ComputeLocationHashesVisitorTest.java | 266 ++++++++++++++++++ .../issue/IntegrateIssuesVisitorTest.java | 4 +- .../issue/TrackerRawInputFactoryTest.java | 167 +---------- .../sonar/server/issue/IssueFieldsSetter.java | 42 ++- .../server/issue/IssueFieldsSetterTest.java | 87 +++++- .../org/sonar/core/issue/DefaultIssue.java | 13 + .../sonar/core/issue/DefaultIssueTest.java | 4 + 10 files changed, 635 insertions(+), 214 deletions(-) create mode 100644 server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitor.java create mode 100644 server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitorTest.java diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java index 36beabb12e4..a90a512cd8a 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java @@ -55,6 +55,7 @@ import org.sonar.ce.task.projectanalysis.filesystem.ComputationTempFolderProvide import org.sonar.ce.task.projectanalysis.issue.BaseIssuesLoader; import org.sonar.ce.task.projectanalysis.issue.CloseIssuesOnRemovedComponentsVisitor; import org.sonar.ce.task.projectanalysis.issue.ClosedIssuesInputFactory; +import org.sonar.ce.task.projectanalysis.issue.ComputeLocationHashesVisitor; import org.sonar.ce.task.projectanalysis.issue.ComponentIssuesLoader; import org.sonar.ce.task.projectanalysis.issue.ComponentIssuesRepositoryImpl; import org.sonar.ce.task.projectanalysis.issue.ComponentsWithUnprocessedIssues; @@ -271,6 +272,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop // debt) RuleTagsCopier.class, IssueCreationDateCalculator.class, + ComputeLocationHashesVisitor.class, DebtCalculator.class, EffortAggregator.class, NewEffortAggregator.class, diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitor.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitor.java new file mode 100644 index 00000000000..7a9f4fdf220 --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitor.java @@ -0,0 +1,215 @@ +/* + * 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.ce.task.projectanalysis.issue; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import org.apache.commons.codec.digest.DigestUtils; +import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; +import org.sonar.ce.task.projectanalysis.source.SourceLinesRepository; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.util.CloseableIterator; +import org.sonar.db.protobuf.DbCommons; +import org.sonar.db.protobuf.DbIssues; +import org.sonar.server.issue.TaintChecker; + +/** + * This visitor will update the locations field of issues, by filling the hashes for all locations. + * It only applies to issues that are taint vulnerabilities and that are new or were changed. + * For performance reasons, it will read each source code file once and feed the lines to all locations in that file. + */ +public class ComputeLocationHashesVisitor extends IssueVisitor { + private static final Pattern MATCH_ALL_WHITESPACES = Pattern.compile("\\s"); + private final List issues = new LinkedList<>(); + private final SourceLinesRepository sourceLinesRepository; + private final TreeRootHolder treeRootHolder; + private final TaintChecker taintChecker; + + public ComputeLocationHashesVisitor(TaintChecker taintChecker, SourceLinesRepository sourceLinesRepository, TreeRootHolder treeRootHolder) { + this.taintChecker = taintChecker; + this.sourceLinesRepository = sourceLinesRepository; + this.treeRootHolder = treeRootHolder; + } + + @Override + public void beforeComponent(Component component) { + issues.clear(); + } + + @Override + public void onIssue(Component component, DefaultIssue issue) { + if (taintChecker.isTaintVulnerability(issue) && !issue.isFromExternalRuleEngine() && (issue.isNew() || issue.locationsChanged())) { + issues.add(issue); + } + } + + @Override + public void afterComponent(Component component) { + Map> locationsByComponent = new HashMap<>(); + List locationsToSet = new LinkedList<>(); + + for (DefaultIssue issue : issues) { + if (issue.getLocations() == null) { + continue; + } + + DbIssues.Locations.Builder primaryLocationBuilder = ((DbIssues.Locations) issue.getLocations()).toBuilder(); + boolean hasTextRange = addLocations(component, locationsByComponent, primaryLocationBuilder); + + // If any location was added (because it had a text range), we'll need to update the issue at the end with the new object containing the hashes + if (hasTextRange) { + locationsToSet.add(new LocationToSet(issue, primaryLocationBuilder)); + } + } + + // Feed lines to locations, component by component + locationsByComponent.forEach(this::updateLocationsInComponent); + + // Finalize by setting hashes + locationsByComponent.values().forEach(list -> list.forEach(Location::afterAllLines)); + + // set new locations to issues + locationsToSet.forEach(LocationToSet::set); + + issues.clear(); + } + + private boolean addLocations(Component component, Map> locationsByComponent, DbIssues.Locations.Builder primaryLocationBuilder) { + boolean hasTextRange = false; + + // Add primary location + if (primaryLocationBuilder.hasTextRange()) { + hasTextRange = true; + PrimaryLocation primaryLocation = new PrimaryLocation(primaryLocationBuilder); + locationsByComponent.computeIfAbsent(component, c -> new LinkedList<>()).add(primaryLocation); + } + + // Add secondary locations + for (DbIssues.Flow.Builder flowBuilder : primaryLocationBuilder.getFlowBuilderList()) { + for (DbIssues.Location.Builder locationBuilder : flowBuilder.getLocationBuilderList()) { + if (locationBuilder.hasTextRange()) { + hasTextRange = true; + Component locationComponent = treeRootHolder.getComponentByUuid(locationBuilder.getComponentId()); + locationsByComponent.computeIfAbsent(locationComponent, c -> new LinkedList<>()).add(new SecondaryLocation(locationBuilder)); + } + } + } + + return hasTextRange; + } + + private void updateLocationsInComponent(Component component, List locations) { + try (CloseableIterator linesIterator = sourceLinesRepository.readLines(component)) { + int lineNumber = 1; + while (linesIterator.hasNext()) { + String line = linesIterator.next(); + for (Location location : locations) { + location.processLine(lineNumber, line); + } + lineNumber++; + } + } + } + + private static class LocationToSet { + private final DefaultIssue issue; + private final DbIssues.Locations.Builder locationsBuilder; + + public LocationToSet(DefaultIssue issue, DbIssues.Locations.Builder locationsBuilder) { + this.issue = issue; + this.locationsBuilder = locationsBuilder; + } + + void set() { + issue.setLocations(locationsBuilder.build()); + } + } + + private static class PrimaryLocation extends Location { + private final DbIssues.Locations.Builder locationsBuilder; + + public PrimaryLocation(DbIssues.Locations.Builder locationsBuilder) { + this.locationsBuilder = locationsBuilder; + } + + @Override + DbCommons.TextRange getTextRange() { + return locationsBuilder.getTextRange(); + } + + @Override + void setHash(String hash) { + locationsBuilder.setChecksum(hash); + } + } + + private static class SecondaryLocation extends Location { + private final DbIssues.Location.Builder locationBuilder; + + public SecondaryLocation(DbIssues.Location.Builder locationBuilder) { + this.locationBuilder = locationBuilder; + } + + @Override + DbCommons.TextRange getTextRange() { + return locationBuilder.getTextRange(); + } + + @Override + void setHash(String hash) { + locationBuilder.setChecksum(hash); + } + } + + private abstract static class Location { + private final StringBuilder hashBuilder = new StringBuilder(); + + abstract DbCommons.TextRange getTextRange(); + + abstract void setHash(String hash); + + public void processLine(int lineNumber, String line) { + DbCommons.TextRange textRange = getTextRange(); + if (lineNumber > textRange.getEndLine() || lineNumber < textRange.getStartLine()) { + return; + } + + if (lineNumber == textRange.getStartLine() && lineNumber == textRange.getEndLine()) { + hashBuilder.append(line, textRange.getStartOffset(), textRange.getEndOffset()); + } else if (lineNumber == textRange.getStartLine()) { + hashBuilder.append(line, textRange.getStartOffset(), line.length()); + } else if (lineNumber < textRange.getEndLine()) { + hashBuilder.append(line); + } else { + hashBuilder.append(line, 0, textRange.getEndOffset()); + } + } + + void afterAllLines() { + String issueContentWithoutWhitespaces = MATCH_ALL_WHITESPACES.matcher(hashBuilder.toString()).replaceAll(""); + String hash = DigestUtils.md5Hex(issueContentWithoutWhitespaces); + setHash(hash); + } + } +} 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 577c15076a2..5b2b75b16b4 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 @@ -27,7 +27,6 @@ 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; @@ -58,25 +57,20 @@ 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 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, - SourceLinesRepository sourceLinesRepository, CommonRuleEngine commonRuleEngine, IssueFilter issueFilter, RuleRepository ruleRepository, - ActiveRulesHolder activeRulesHolder) { + public TrackerRawInputFactory(TreeRootHolder treeRootHolder, BatchReportReader reportReader, SourceLinesHashRepository sourceLinesHash, 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; @@ -195,7 +189,6 @@ public class TrackerRawInputFactory { if (reportIssue.hasTextRange()) { 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) { @@ -305,48 +298,10 @@ public class TrackerRawInputFactory { 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 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()); diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitorTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitorTest.java new file mode 100644 index 00000000000..5fdb62f1238 --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitorTest.java @@ -0,0 +1,266 @@ +/* + * 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.ce.task.projectanalysis.issue; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +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.sonar.api.config.Configuration; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.RuleType; +import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.component.ReportComponent; +import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule; +import org.sonar.ce.task.projectanalysis.source.SourceLinesRepository; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.util.CloseableIterator; +import org.sonar.db.protobuf.DbCommons; +import org.sonar.db.protobuf.DbIssues; +import org.sonar.server.issue.TaintChecker; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class ComputeLocationHashesVisitorTest { + 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 RuleKey RULE_KEY = RuleKey.of("javasecurity", "S001"); + private static final Component FILE_1 = ReportComponent.builder(Component.Type.FILE, 2).build(); + private static final Component FILE_2 = ReportComponent.builder(Component.Type.FILE, 3).build(); + private static final Component ROOT = ReportComponent.builder(Component.Type.PROJECT, 1) + .addChildren(FILE_1, FILE_2) + .build(); + + private final SourceLinesRepository sourceLinesRepository = mock(SourceLinesRepository.class); + private final MutableConfiguration configuration = new MutableConfiguration(); + private final TaintChecker taintChecker = new TaintChecker(configuration); + @Rule + public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule(); + private final ComputeLocationHashesVisitor underTest = new ComputeLocationHashesVisitor(taintChecker, sourceLinesRepository, treeRootHolder); + + @Before + public void before() { + Iterator stringIterator = IntStream.rangeClosed(1, 9) + .mapToObj(i -> String.format(EXAMPLE_LINE_OF_CODE_FORMAT, i)) + .iterator(); + when(sourceLinesRepository.readLines(FILE_1)).thenReturn(CloseableIterator.from(stringIterator)); + when(sourceLinesRepository.readLines(FILE_2)).thenReturn(newOneLineIterator(LINE_IN_ANOTHER_FILE)); + treeRootHolder.setRoot(ROOT); + } + + @Test + public void do_nothing_if_issue_is_unchanged() { + DefaultIssue issue = createIssue() + .setLocationsChanged(false) + .setNew(false) + .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 3, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build()); + + underTest.beforeComponent(FILE_1); + underTest.onIssue(FILE_1, issue); + underTest.afterComponent(FILE_1); + + DbIssues.Locations locations = issue.getLocations(); + assertThat(locations.getChecksum()).isEmpty(); + verifyNoInteractions(sourceLinesRepository); + } + + @Test + public void do_nothing_if_issue_is_not_taint_vulnerability() { + DefaultIssue issue = createIssue() + .setRuleKey(RuleKey.of("repo", "rule")) + .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 3, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build()); + + underTest.onIssue(FILE_1, issue); + underTest.afterComponent(FILE_1); + + DbIssues.Locations locations = issue.getLocations(); + assertThat(locations.getChecksum()).isEmpty(); + verifyNoInteractions(sourceLinesRepository); + } + + @Test + public void calculates_hash_for_multiple_lines() { + DefaultIssue issue = createIssue() + .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 3, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build()); + + underTest.onIssue(FILE_1, issue); + underTest.afterComponent(FILE_1); + + assertLocationHashIsMadeOf(issue, "intexample=line+of+code+1;intexample=line+of+code+2;intexample=line+of+code+3;"); + } + + @Test + public void calculates_hash_for_multiple_files() { + DefaultIssue issue1 = createIssue() + .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 3, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build()); + DefaultIssue issue2 = createIssue() + .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 1, LINE_IN_ANOTHER_FILE.length())).build()); + + underTest.onIssue(FILE_1, issue1); + underTest.afterComponent(FILE_1); + + underTest.onIssue(FILE_2, issue2); + underTest.afterComponent(FILE_2); + + assertLocationHashIsMadeOf(issue1, "intexample=line+of+code+1;intexample=line+of+code+2;intexample=line+of+code+3;"); + assertLocationHashIsMadeOf(issue2, "Stringstring='line-in-the-another-file';"); + } + + @Test + public void calculates_hash_for_partial_line() { + DefaultIssue issue = createIssue() + .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 13, 1, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build()); + + underTest.onIssue(FILE_1, issue); + underTest.afterComponent(FILE_1); + + assertLocationHashIsMadeOf(issue, "line+of+code+1;"); + } + + @Test + public void calculates_hash_for_partial_multiple_lines() { + DefaultIssue issue = createIssue() + .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 13, 3, 11)).build()); + + underTest.onIssue(FILE_1, issue); + underTest.afterComponent(FILE_1); + + assertLocationHashIsMadeOf(issue, "line+of+code+1;intexample=line+of+code+2;intexample"); + } + + @Test + public void dont_calculate_hash_if_no_textRange() { + // primary location and one of the secondary locations have no text range + DefaultIssue issue = createIssue() + .setLocations(DbIssues.Locations.newBuilder() + .addFlow(DbIssues.Flow.newBuilder() + .addLocation(DbIssues.Location.newBuilder() + .setTextRange(createRange(1, 0, 1, LINE_IN_THE_MAIN_FILE.length())) + .setComponentId(FILE_1.getUuid()) + .build()) + .addLocation(DbIssues.Location.newBuilder() + .setComponentId(FILE_2.getUuid()) + .build()) + .build()) + .build()); + + when(sourceLinesRepository.readLines(FILE_1)).thenReturn(newOneLineIterator(LINE_IN_THE_MAIN_FILE)); + + underTest.onIssue(FILE_1, issue); + underTest.afterComponent(FILE_1); + + verify(sourceLinesRepository).readLines(FILE_1); + verifyNoMoreInteractions(sourceLinesRepository); + 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()).isEmpty(); + } + + @Test + public void calculates_hash_for_multiple_locations() { + DefaultIssue issue = createIssue() + .setLocations(DbIssues.Locations.newBuilder() + .setTextRange(createRange(1, 0, 1, LINE_IN_THE_MAIN_FILE.length())) + .addFlow(DbIssues.Flow.newBuilder() + .addLocation(DbIssues.Location.newBuilder() + .setTextRange(createRange(1, 0, 1, LINE_IN_THE_MAIN_FILE.length())) + .setComponentId(FILE_1.getUuid()) + .build()) + .addLocation(DbIssues.Location.newBuilder() + .setTextRange(createRange(1, 0, 1, LINE_IN_ANOTHER_FILE.length())) + .setComponentId(FILE_2.getUuid()) + .build()) + .build()) + .build()); + + when(sourceLinesRepository.readLines(FILE_1)).thenReturn(newOneLineIterator(LINE_IN_THE_MAIN_FILE)); + when(sourceLinesRepository.readLines(FILE_2)).thenReturn(newOneLineIterator(LINE_IN_ANOTHER_FILE)); + + underTest.onIssue(FILE_1, issue); + underTest.afterComponent(FILE_1); + + 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 DbCommons.TextRange createRange(int startLine, int startOffset, int endLine, int endOffset) { + return DbCommons.TextRange.newBuilder() + .setStartLine(startLine).setStartOffset(startOffset) + .setEndLine(endLine).setEndOffset(endOffset) + .build(); + } + + private DefaultIssue createIssue() { + return new DefaultIssue() + .setLocationsChanged(true) + .setRuleKey(RULE_KEY) + .setIsFromExternalRuleEngine(false) + .setType(RuleType.CODE_SMELL); + } + + private void assertLocationHashIsMadeOf(DefaultIssue issue, String stringToHash) { + String expectedHash = DigestUtils.md5Hex(stringToHash); + DbIssues.Locations locations = issue.getLocations(); + assertThat(locations.getChecksum()).isEqualTo(expectedHash); + } + + private CloseableIterator newOneLineIterator(String lineContent) { + return CloseableIterator.from(List.of(lineContent).iterator()); + } + + private static class MutableConfiguration implements Configuration { + private final Map keyValues = new HashMap<>(); + + public Configuration put(String key, String value) { + keyValues.put(key, value.trim()); + return this; + } + + @Override + public Optional get(String key) { + return Optional.ofNullable(keyValues.get(key)); + } + + @Override + public boolean hasKey(String key) { + return keyValues.containsKey(key); + } + + @Override + public String[] getStringArray(String key) { + throw new UnsupportedOperationException("getStringArray not implemented"); + } + } +} 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 7e261b7ad72..454a56f4074 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 @@ -150,8 +150,8 @@ public class IntegrateIssuesVisitorTest { when(movedFilesRepository.getOriginalFile(any(Component.class))).thenReturn(Optional.empty()); DbClient dbClient = dbTester.getDbClient(); - TrackerRawInputFactory rawInputFactory = new TrackerRawInputFactory(treeRootHolder, reportReader, sourceLinesHash, sourceLinesRepository, - new CommonRuleEngineImpl(), issueFilter, ruleRepositoryRule, activeRulesHolder); + TrackerRawInputFactory rawInputFactory = new TrackerRawInputFactory(treeRootHolder, reportReader, sourceLinesHash, new CommonRuleEngineImpl(), issueFilter, + ruleRepositoryRule, activeRulesHolder); TrackerBaseInputFactory baseInputFactory = new TrackerBaseInputFactory(issuesLoader, dbClient, movedFilesRepository, mock(ReportModulesPath.class), analysisMetadataHolder, new IssueFieldsSetter(), mock(ComponentsWithUnprocessedIssues.class)); TrackerTargetBranchInputFactory targetInputFactory = new TrackerTargetBranchInputFactory(issuesLoader, targetBranchComponentUuids, dbClient); 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 1392fd64f86..3f0b1f4c289 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 @@ -75,8 +75,7 @@ public class TrackerRawInputFactoryTest { private static final String FILE_UUID = "fake_uuid"; private static final String ANOTHER_FILE_UUID = "another_fake_uuid"; 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; @@ -96,19 +95,10 @@ public class TrackerRawInputFactoryTest { 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)); - } + commonRuleEngine, issueFilter, ruleRepository, activeRulesHolder); @Test public void load_source_hash_sequences() { @@ -168,8 +158,6 @@ public class TrackerRawInputFactoryTest { assertInitializedIssue(issue); assertThat(issue.effort()).isNull(); assertThat(issue.getRuleDescriptionContextKey()).isEmpty(); - - assertLocationHashIsMadeOf(input, "intexample=line+of+code+2;"); } @Test @@ -196,121 +184,6 @@ public class TrackerRawInputFactoryTest { .containsOnly(Optional.of(TEST_CONTEXT_KEY)); } - @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 - 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 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 newOneLineIterator(String lineContent) { - return CloseableIterator.from(List.of(lineContent).iterator()); - } - @Test public void set_rule_name_as_message_when_issue_message_from_report_is_empty() { RuleKey ruleKey = RuleKey.of("java", "S001"); @@ -556,6 +429,15 @@ public class TrackerRawInputFactoryTest { assertThat(input.getIssues()).isEmpty(); } + private ScannerReport.TextRange newTextRange(int issueOnLine) { + return ScannerReport.TextRange.newBuilder() + .setStartLine(issueOnLine) + .setEndLine(issueOnLine) + .setStartOffset(0) + .setEndOffset(EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1) + .build(); + } + private void assertInitializedIssue(DefaultIssue issue) { assertInitializedExternalIssue(issue, STATUS_OPEN); assertThat(issue.effort()).isNull(); @@ -581,31 +463,4 @@ public class TrackerRawInputFactoryTest { dumbRule.setName(name); 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 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-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 2b027eb37ac..1b7afe26d32 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 @@ -35,6 +35,7 @@ import org.sonar.api.utils.Duration; import org.sonar.core.issue.DefaultIssue; import org.sonar.core.issue.DefaultIssueComment; import org.sonar.core.issue.IssueChangeContext; +import org.sonar.db.protobuf.DbIssues; import org.sonar.db.user.UserDto; import static com.google.common.base.Preconditions.checkState; @@ -171,15 +172,54 @@ public class IssueFieldsSetter { return false; } + /** + * New value will be set if the locations are different, ignoring the hashes. If that's the case, we mark the issue as changed, + * and we also flag that the locations have changed, so that we calculate all the hashes later, in an efficient way. + * WARNING: It is possible that the hashes changes without the text ranges changing, but for optimization we take that risk. + * + * @see ComputeLocationHashesVisitor + */ public boolean setLocations(DefaultIssue issue, @Nullable Object locations) { - if (!Objects.equals(locations, issue.getLocations())) { + if (!locationsEqualsIgnoreHashes(locations, issue.getLocations())) { issue.setLocations(locations); issue.setChanged(true); + issue.setLocationsChanged(true); return true; } return false; } + private static boolean locationsEqualsIgnoreHashes(@Nullable Object l1, @Nullable DbIssues.Locations l2) { + if (l1 == null && l2 == null) { + return true; + } + + if (l2 == null || !(l1 instanceof DbIssues.Locations)) { + return false; + } + + DbIssues.Locations l1c = (DbIssues.Locations) l1; + if (!Objects.equals(l1c.getTextRange(), l2.getTextRange()) || l1c.getFlowCount() != l2.getFlowCount()) { + return false; + } + + for (int i = 0; i < l1c.getFlowCount(); i++) { + if (l1c.getFlow(i).getLocationCount() != l2.getFlow(i).getLocationCount()) { + return false; + } + for (int j = 0; j < l1c.getFlow(i).getLocationCount(); j++) { + if (!locationEqualsIgnoreHashes(l1c.getFlow(i).getLocation(j), l2.getFlow(i).getLocation(j))) { + return false; + } + } + } + return true; + } + + private static boolean locationEqualsIgnoreHashes(DbIssues.Location l1, DbIssues.Location l2) { + return Objects.equals(l1.getComponentId(), l2.getComponentId()) && Objects.equals(l1.getTextRange(), l2.getTextRange()) && Objects.equals(l1.getMsg(), l2.getMsg()); + } + public boolean setPastLocations(DefaultIssue issue, @Nullable Object previousLocations) { Object currentLocations = issue.getLocations(); issue.setLocations(previousLocations); 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 c0835a1b701..65801c0e591 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 @@ -28,6 +28,8 @@ import org.sonar.api.utils.Duration; import org.sonar.core.issue.DefaultIssue; 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.user.UserDto; import static org.assertj.core.api.Assertions.assertThat; @@ -269,21 +271,90 @@ public class IssueFieldsSetterTest { } @Test - public void change_locations() { - issue.setLocations("[1-3]"); - boolean updated = underTest.setLocations(issue, "[1-4]"); + public void change_locations_if_primary_text_rage_changed() { + DbCommons.TextRange range = DbCommons.TextRange.newBuilder().setStartLine(1).build(); + DbIssues.Locations locations = DbIssues.Locations.newBuilder() + .setTextRange(range) + .build(); + DbIssues.Locations locations2 = locations.toBuilder().setTextRange(range.toBuilder().setEndLine(2).build()).build(); + issue.setLocations(locations); + boolean updated = underTest.setLocations(issue, locations2); assertThat(updated).isTrue(); - assertThat(issue.getLocations().toString()).isEqualTo("[1-4]"); + assertThat((Object) issue.getLocations()).isEqualTo(locations2); + assertThat(issue.locationsChanged()).isTrue(); assertThat(issue.currentChange()).isNull(); assertThat(issue.mustSendNotifications()).isFalse(); } @Test - public void do_not_change_locations() { - issue.setLocations("[1-3]"); - boolean updated = underTest.setLocations(issue, "[1-3]"); + public void change_locations_if_secondary_text_rage_changed() { + DbCommons.TextRange range = DbCommons.TextRange.newBuilder().setStartLine(1).build(); + DbIssues.Locations locations = DbIssues.Locations.newBuilder() + .addFlow(DbIssues.Flow.newBuilder() + .addLocation(DbIssues.Location.newBuilder().setTextRange(range)) + .build()) + .build(); + issue.setLocations(locations); + DbIssues.Locations.Builder builder = locations.toBuilder(); + builder.getFlowBuilder(0).getLocationBuilder(0).setTextRange(range.toBuilder().setEndLine(2)); + boolean updated = underTest.setLocations(issue, builder.build()); + assertThat(updated).isTrue(); + } + + @Test + public void change_locations_if_secondary_message_changed() { + DbIssues.Locations locations = DbIssues.Locations.newBuilder() + .addFlow(DbIssues.Flow.newBuilder() + .addLocation(DbIssues.Location.newBuilder().setMsg("msg1")) + .build()) + .build(); + issue.setLocations(locations); + DbIssues.Locations.Builder builder = locations.toBuilder(); + builder.getFlowBuilder(0).getLocationBuilder(0).setMsg("msg2"); + boolean updated = underTest.setLocations(issue, builder.build()); + assertThat(updated).isTrue(); + } + + @Test + public void change_locations_if_different_flow_count() { + DbIssues.Locations locations = DbIssues.Locations.newBuilder() + .addFlow(DbIssues.Flow.newBuilder() + .addLocation(DbIssues.Location.newBuilder()) + .build()) + .build(); + issue.setLocations(locations); + DbIssues.Locations.Builder builder = locations.toBuilder(); + builder.clearFlow(); + boolean updated = underTest.setLocations(issue, builder.build()); + assertThat(updated).isTrue(); + } + + @Test + public void do_not_change_locations_if_primary_hash_changed() { + DbCommons.TextRange range = DbCommons.TextRange.newBuilder().setStartLine(1).build(); + DbIssues.Locations locations = DbIssues.Locations.newBuilder() + .setTextRange(range) + .setChecksum("1") + .build(); + issue.setLocations(locations); + boolean updated = underTest.setLocations(issue, locations.toBuilder().setChecksum("2").build()); + assertThat(updated).isFalse(); + } + + @Test + public void do_not_change_locations_if_secondary_hash_changed() { + DbCommons.TextRange range = DbCommons.TextRange.newBuilder().setStartLine(1).build(); + DbIssues.Locations locations = DbIssues.Locations.newBuilder() + .addFlow(DbIssues.Flow.newBuilder() + .addLocation(DbIssues.Location.newBuilder().setTextRange(range)) + .build()) + .setChecksum("1") + .build(); + issue.setLocations(locations); + DbIssues.Locations.Builder builder = locations.toBuilder(); + builder.getFlowBuilder(0).getLocationBuilder(0).setChecksum("2"); + boolean updated = underTest.setLocations(issue, builder.build()); assertThat(updated).isFalse(); - assertThat(issue.getLocations().toString()).isEqualTo("[1-3]"); assertThat(issue.currentChange()).isNull(); assertThat(issue.mustSendNotifications()).isFalse(); } 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 4f92924e0c4..b48470bcc01 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 @@ -104,6 +104,9 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure. // true if the issue is being copied between branch private boolean isCopied = false; + // true if any of the locations have changed (ignoring hashes) + private boolean locationsChanged = false; + // True if the issue did exist in the previous scan but not in the current one. That means // that this issue should be closed. private boolean beingClosed = false; @@ -399,6 +402,16 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure. return this; } + public boolean locationsChanged() { + return locationsChanged; + } + + public DefaultIssue setLocationsChanged(boolean locationsChanged) { + this.locationsChanged = locationsChanged; + return this; + } + + public DefaultIssue setNew(boolean b) { isNew = b; return this; diff --git a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java index e63651e7a43..2a5091278b8 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java @@ -54,6 +54,8 @@ public class DefaultIssueTest { .setAssigneeUuid("julien") .setAuthorLogin("steph") .setChecksum("c7b5db46591806455cf082bb348631e8") + .setLocations("loc") + .setLocationsChanged(true) .setNew(true) .setIsOnChangedLine(true) .setIsNewCodeReferenceIssue(true) @@ -69,6 +71,8 @@ public class DefaultIssueTest { .setSelectedAt(1400000000000L) .setRuleDescriptionContextKey(TEST_CONTEXT_KEY); + assertThat((Object) issue.getLocations()).isEqualTo("loc"); + assertThat(issue.locationsChanged()).isTrue(); assertThat(issue.key()).isEqualTo("ABCD"); assertThat(issue.componentKey()).isEqualTo("org.sample.Sample"); assertThat(issue.projectKey()).isEqualTo("Sample"); -- 2.39.5