*/
package org.sonar.core.issue;
+import java.util.Date;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
private final RuleKey ruleKey;
private final String status;
private final String branchName;
+ private final Date creationDate;
- public ShortBranchIssue(String key, @Nullable Integer line, String message, @Nullable String lineHash, RuleKey ruleKey, String status, String branchName) {
+ public ShortBranchIssue(String key, @Nullable Integer line, String message, @Nullable String lineHash, RuleKey ruleKey, String status, String branchName, Date creationDate) {
this.key = key;
this.line = line;
this.message = message;
this.ruleKey = ruleKey;
this.status = status;
this.branchName = branchName;
+ this.creationDate = creationDate;
}
public String getKey() {
return branchName;
}
+ @Override
+ public Date getCreationDate() {
+ return creationDate;
+ }
+
@Override
public int hashCode() {
return key.hashCode();
mapper(dbSession).scrollNonClosedByModuleOrProject(module.projectUuid(), likeModuleUuidPath, handler);
}
- public List<ShortBranchIssueDto> selectResolvedOrConfirmedByComponentUuids(DbSession dbSession, Collection<String> componentUuids) {
- return executeLargeInputs(componentUuids, mapper(dbSession)::selectResolvedOrConfirmedByComponentUuids);
+ public List<ShortBranchIssueDto> selectOpenByComponentUuids(DbSession dbSession, Collection<String> componentUuids) {
+ return executeLargeInputs(componentUuids, mapper(dbSession)::selectOpenByComponentUuids);
}
public void insert(DbSession session, IssueDto dto) {
List<IssueDto> selectByKeys(List<String> keys);
- List<ShortBranchIssueDto> selectResolvedOrConfirmedByComponentUuids(List<String> componentUuids);
+ List<ShortBranchIssueDto> selectOpenByComponentUuids(List<String> componentUuids);
void insert(IssueDto issue);
import org.sonar.api.rule.RuleKey;
import org.sonar.core.issue.ShortBranchIssue;
+import static org.sonar.api.utils.DateUtils.longToDate;
+
public final class ShortBranchIssueDto implements Serializable {
private String kee;
private Integer line;
private String checksum;
private String status;
+ private Long issueCreationDate;
// joins
private String ruleKey;
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
- public static ShortBranchIssue toShortBranchIssue(ShortBranchIssueDto dto) {
- return new ShortBranchIssue(dto.getKey(), dto.getLine(), dto.getMessage(), dto.getChecksum(), dto.getRuleKey(), dto.getStatus(), dto.getBranchName());
+ public ShortBranchIssue toShortBranchIssue() {
+ return new ShortBranchIssue(getKey(), getLine(), getMessage(), getChecksum(), getRuleKey(), getStatus(), getBranchName(), longToDate(issueCreationDate));
}
}
ON i.project_uuid = b.uuid
AND b.branch_type = 'SHORT'
AND b.merge_branch_uuid = #{mergeBranchUuid,jdbcType=VARCHAR}
- AND (i.status = 'CONFIRMED' OR i.status = 'RESOLVED')
+ AND i.status != 'CLOSED'
</select>
</mapper>
</foreach>
</select>
- <select id="selectResolvedOrConfirmedByComponentUuids" parameterType="map" resultType="ShortBranchIssue">
+ <select id="selectOpenByComponentUuids" parameterType="map" resultType="ShortBranchIssue">
select
i.kee as kee,
i.message as message,
i.line as line,
i.status as status,
i.checksum as checksum,
+ i.issue_creation_date as issueCreationDate,
r.plugin_rule_key as ruleKey,
r.plugin_name as ruleRepo,
b.kee as branchName
<foreach collection="list" open="(" close=")" item="key" separator=",">
#{key,jdbcType=VARCHAR}
</foreach>
- and (i.status = 'CONFIRMED' OR i.status = 'RESOLVED')
+ and i.status <> 'CLOSED'
</select>
<select id="scrollNonClosedByModuleOrProject" parameterType="map" resultType="Issue" fetchSize="${_scrollFetchSize}" resultSetType="FORWARD_ONLY">
}
@Test
- public void selectResolvedOrConfirmedByComponentUuid() {
+ public void selectOpenByComponentUuid() {
RuleDefinitionDto rule = db.rules().insert();
ComponentDto project = db.components().insertMainBranch();
ComponentDto projectBranch = db.components().insertProjectBranch(project,
IssueDto wontfixIssue = db.issues().insert(rule, projectBranch, file, i -> i.setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_WONT_FIX));
IssueDto fpIssue = db.issues().insert(rule, projectBranch, file, i -> i.setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_FALSE_POSITIVE));
- assertThat(underTest.selectResolvedOrConfirmedByComponentUuids(db.getSession(), Collections.singletonList(file.uuid())))
+ assertThat(underTest.selectOpenByComponentUuids(db.getSession(), Collections.singletonList(file.uuid())))
.extracting("kee")
- .containsOnly(confirmedIssue.getKey(), wontfixIssue.getKey(), fpIssue.getKey());
+ .containsOnly(openIssue.getKey(), reopenedIssue.getKey(), confirmedIssue.getKey(), wontfixIssue.getKey(), fpIssue.getKey());
}
@Test
- public void selectResolvedOrConfirmedByComponentUuid_should_correctly_map_required_fields() {
+ public void selectOpenByComponentUuid_should_correctly_map_required_fields() {
RuleDefinitionDto rule = db.rules().insert();
ComponentDto project = db.components().insertMainBranch();
ComponentDto projectBranch = db.components().insertProjectBranch(project,
ComponentDto file = db.components().insertComponent(newFileDto(projectBranch));
IssueDto fpIssue = db.issues().insert(rule, projectBranch, file, i -> i.setStatus("RESOLVED").setResolution("FALSE-POSITIVE"));
- ShortBranchIssueDto fp = underTest.selectResolvedOrConfirmedByComponentUuids(db.getSession(), Collections.singletonList(file.uuid())).get(0);
+ ShortBranchIssueDto fp = underTest.selectOpenByComponentUuids(db.getSession(), Collections.singletonList(file.uuid())).get(0);
assertThat(fp.getLine()).isEqualTo(fpIssue.getLine());
assertThat(fp.getMessage()).isEqualTo(fpIssue.getMessage());
assertThat(fp.getChecksum()).isEqualTo(fpIssue.getChecksum());
return Collections.emptyList();
}
try (DbSession session = dbClient.openSession(false)) {
- return dbClient.issueDao().selectResolvedOrConfirmedByComponentUuids(session, uuids)
+ return dbClient.issueDao().selectOpenByComponentUuids(session, uuids)
.stream()
.map(ShortBranchIssueDto::toShortBranchIssue)
.collect(Collectors.toList());
setRootUuid(long1.uuid());
assertThat(underTest.getUuids(fileWithNoIssues.getKey())).isEmpty();
- assertThat(underTest.getUuids(fileWithOneOpenIssue.getKey())).isEmpty();
+ assertThat(underTest.getUuids(fileWithOneOpenIssue.getKey())).containsOnly(fileWithOneOpenIssue.uuid());
assertThat(underTest.getUuids(fileWithOneResolvedIssue.getKey())).containsOnly(fileWithOneResolvedIssue.uuid());
assertThat(underTest.getUuids(fileWithOneOpenTwoResolvedIssues.getKey())).containsOnly(fileWithOneOpenTwoResolvedIssues.uuid());
assertThat(underTest.getUuids(fileWithOneResolvedIssue.getKey())).isEmpty();
assertThat(underTest.getUuids(fileWithOneResolvedIssueOnLong2.getKey())).containsOnly(fileWithOneResolvedIssueOnLong2.uuid());
- assertThat(underTest.getUuids(fileWithOneOpenIssueOnLong2.getKey())).isEmpty();
+ assertThat(underTest.getUuids(fileWithOneOpenIssueOnLong2.getKey())).containsOnly(fileWithOneOpenIssueOnLong2.uuid());
}
@Test
package org.sonar.server.computation.task.projectanalysis.issue;
import com.google.common.collect.ImmutableMap;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.Date;
import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
@Test
public void do_nothing_if_no_match() {
when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Collections.emptyList());
- DefaultIssue i = createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null);
+ DefaultIssue i = createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null, new Date());
copier.tryMerge(component, Collections.singleton(i));
verify(resolvedShortBranchIssuesLoader).loadCandidateIssuesForMergingInTargetBranch(component);
@Test
public void do_nothing_if_no_new_issue() {
- DefaultIssue i = createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null);
+ DefaultIssue i = createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null, new Date());
when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Collections.singleton(newShortBranchIssue(i, "myBranch")));
copier.tryMerge(component, Collections.emptyList());
@Test
public void update_status_on_matches() {
- DefaultIssue issue1 = createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null);
+ DefaultIssue issue1 = createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null, new Date());
ShortBranchIssue shortBranchIssue = newShortBranchIssue(issue1, "myBranch");
- DefaultIssue newIssue = createIssue("issue2", "rule1", Issue.STATUS_OPEN, null);
+ DefaultIssue newIssue = createIssue("issue2", "rule1", Issue.STATUS_OPEN, null, new Date());
when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Collections.singleton(shortBranchIssue));
when(resolvedShortBranchIssuesLoader.loadDefaultIssuesWithChanges(anyListOf(ShortBranchIssue.class))).thenReturn(ImmutableMap.of(shortBranchIssue, issue1));
@Test
public void prefer_resolved_issues() {
- ShortBranchIssue shortBranchIssue1 = newShortBranchIssue(createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null), "myBranch1");
- ShortBranchIssue shortBranchIssue2 = newShortBranchIssue(createIssue("issue2", "rule1", Issue.STATUS_CONFIRMED, null), "myBranch2");
- DefaultIssue issue3 = createIssue("issue3", "rule1", Issue.STATUS_RESOLVED, Issue.RESOLUTION_FALSE_POSITIVE);
+ ShortBranchIssue shortBranchIssue1 = newShortBranchIssue(createIssue("issue1", "rule1", Issue.STATUS_REOPENED, null, new Date()), "myBranch1");
+ ShortBranchIssue shortBranchIssue2 = newShortBranchIssue(createIssue("issue2", "rule1", Issue.STATUS_CONFIRMED, null, new Date()), "myBranch2");
+ DefaultIssue issue3 = createIssue("issue3", "rule1", Issue.STATUS_RESOLVED, Issue.RESOLUTION_FALSE_POSITIVE, new Date());
ShortBranchIssue shortBranchIssue3 = newShortBranchIssue(issue3, "myBranch3");
- DefaultIssue newIssue = createIssue("newIssue", "rule1", Issue.STATUS_OPEN, null);
+ DefaultIssue newIssue = createIssue("newIssue", "rule1", Issue.STATUS_OPEN, null, new Date());
when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Arrays.asList(shortBranchIssue1, shortBranchIssue2, shortBranchIssue3));
when(resolvedShortBranchIssuesLoader.loadDefaultIssuesWithChanges(anyListOf(ShortBranchIssue.class))).thenReturn(ImmutableMap.of(shortBranchIssue3, issue3));
verify(issueLifecycle).mergeConfirmedOrResolvedFromShortLivingBranch(newIssue, issue3, "myBranch3");
}
- private static DefaultIssue createIssue(String key, String ruleKey, String status, @Nullable String resolution) {
+ @Test
+ public void prefer_confirmed_issues() {
+ ShortBranchIssue shortBranchIssue1 = newShortBranchIssue(createIssue("issue1", "rule1", Issue.STATUS_REOPENED, null, new Date()), "myBranch1");
+ ShortBranchIssue shortBranchIssue2 = newShortBranchIssue(createIssue("issue2", "rule1", Issue.STATUS_OPEN, null, new Date()), "myBranch2");
+ DefaultIssue issue3 = createIssue("issue3", "rule1", Issue.STATUS_CONFIRMED, null, new Date());
+ ShortBranchIssue shortBranchIssue3 = newShortBranchIssue(issue3, "myBranch3");
+ DefaultIssue newIssue = createIssue("newIssue", "rule1", Issue.STATUS_OPEN, null, new Date());
+
+ when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Arrays.asList(shortBranchIssue1, shortBranchIssue2, shortBranchIssue3));
+ when(resolvedShortBranchIssuesLoader.loadDefaultIssuesWithChanges(anyListOf(ShortBranchIssue.class))).thenReturn(ImmutableMap.of(shortBranchIssue3, issue3));
+ copier.tryMerge(component, Collections.singleton(newIssue));
+ verify(issueLifecycle).mergeConfirmedOrResolvedFromShortLivingBranch(newIssue, issue3, "myBranch3");
+ }
+
+ @Test
+ public void prefer_older_issues() {
+ Instant now = Instant.now();
+ ShortBranchIssue shortBranchIssue1 = newShortBranchIssue(createIssue("issue1", "rule1", Issue.STATUS_REOPENED, null, Date.from(now.plus(2, ChronoUnit.SECONDS))), "myBranch1");
+ ShortBranchIssue shortBranchIssue2 = newShortBranchIssue(createIssue("issue2", "rule1", Issue.STATUS_OPEN, null, Date.from(now.plus(1, ChronoUnit.SECONDS))), "myBranch2");
+ DefaultIssue issue3 = createIssue("issue3", "rule1", Issue.STATUS_OPEN, null, Date.from(now));
+ ShortBranchIssue shortBranchIssue3 = newShortBranchIssue(issue3, "myBranch3");
+ DefaultIssue newIssue = createIssue("newIssue", "rule1", Issue.STATUS_OPEN, null, new Date());
+
+ when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Arrays.asList(shortBranchIssue1, shortBranchIssue2, shortBranchIssue3));
+ when(resolvedShortBranchIssuesLoader.loadDefaultIssuesWithChanges(anyListOf(ShortBranchIssue.class))).thenReturn(ImmutableMap.of(shortBranchIssue3, issue3));
+ copier.tryMerge(component, Collections.singleton(newIssue));
+ verify(issueLifecycle).mergeConfirmedOrResolvedFromShortLivingBranch(newIssue, issue3, "myBranch3");
+ }
+
+ private static DefaultIssue createIssue(String key, String ruleKey, String status, @Nullable String resolution, Date creationDate) {
DefaultIssue issue = new DefaultIssue();
issue.setKey(key);
issue.setRuleKey(RuleKey.of("repo", ruleKey));
issue.setLine(1);
issue.setStatus(status);
issue.setResolution(resolution);
+ issue.setCreationDate(creationDate);
return issue;
}
private ShortBranchIssue newShortBranchIssue(DefaultIssue i, String originBranch) {
- return new ShortBranchIssue(i.key(), i.line(), i.message(), i.getLineHash(), i.ruleKey(), i.status(), originBranch);
+ return new ShortBranchIssue(i.key(), i.line(), i.message(), i.getLineHash(), i.ruleKey(), i.status(), originBranch, i.creationDate());
}
}
public String getStatus() {
return status;
}
+
+ @Override
+ public Date getCreationDate() {
+ return creationDate;
+ }
}
import org.sonar.api.issue.Issue;
import org.sonar.api.rule.RuleKey;
+import static java.util.Comparator.comparing;
+
public class AbstractTracker<RAW extends Trackable, BASE extends Trackable> {
protected void match(Tracking<RAW, BASE> tracking, Function<Trackable, SearchKey> searchKeyFactory) {
- match(tracking, searchKeyFactory, false);
- }
-
- protected void match(Tracking<RAW, BASE> tracking, Function<Trackable, SearchKey> searchKeyFactory, boolean preferResolved) {
if (tracking.isComplete()) {
return;
for (RAW raw : tracking.getUnmatchedRaws()) {
SearchKey rawKey = searchKeyFactory.apply(raw);
Collection<BASE> bases = baseSearch.get(rawKey);
- if (!bases.isEmpty()) {
- BASE match;
- if (preferResolved) {
- match = bases.stream()
- .filter(i -> Issue.STATUS_RESOLVED.equals(i.getStatus()))
- .findFirst()
- .orElse(bases.iterator().next());
- } else {
- // TODO taking the first one. Could be improved if there are more than 2 issues on the same line.
- // Message could be checked to take the best one.
- match = bases.iterator().next();
- }
- tracking.match(raw, match);
- baseSearch.remove(rawKey, match);
- }
+ bases.stream()
+ .sorted(comparing(this::statusRank).reversed()
+ .thenComparing(comparing(Trackable::getCreationDate)))
+ .findFirst()
+ .ifPresent(match -> {
+ tracking.match(raw, match);
+ baseSearch.remove(rawKey, match);
+ });
+ }
+ }
+
+ private int statusRank(BASE i) {
+ switch (i.getStatus()) {
+ case Issue.STATUS_RESOLVED:
+ return 2;
+ case Issue.STATUS_CONFIRMED:
+ return 1;
+ default:
+ return 0;
}
}
Tracking<RAW, BASE> tracking = new Tracking<>(rawInput, baseInput);
// 1. match issues with same rule, same line and same line hash, but not necessarily with same message
- match(tracking, LineAndLineHashKey::new, true);
+ match(tracking, LineAndLineHashKey::new);
// 2. match issues with same rule, same message and same line hash
- match(tracking, LineHashAndMessageKey::new, true);
+ match(tracking, LineHashAndMessageKey::new);
return tracking;
}
*/
package org.sonar.core.issue.tracking;
+import java.util.Date;
import javax.annotation.CheckForNull;
import org.sonar.api.rule.RuleKey;
+import org.sonar.core.issue.DefaultIssue;
public interface Trackable {
RuleKey getRuleKey();
String getStatus();
+
+ /**
+ * Functional creation date for the issue. See {@link DefaultIssue#creationDate()}
+ */
+ Date getCreationDate();
}
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Date;
import java.util.List;
import javax.annotation.Nullable;
import org.apache.commons.codec.digest.DigestUtils;
@Test
public void do_not_fail_if_raw_line_does_not_exist() {
FakeInput baseInput = new FakeInput();
- FakeInput rawInput = new FakeInput("H1").addIssue(new Issue(200, "H200", RULE_SYSTEM_PRINT, "msg", org.sonar.api.issue.Issue.STATUS_OPEN));
+ FakeInput rawInput = new FakeInput("H1").addIssue(new Issue(200, "H200", RULE_SYSTEM_PRINT, "msg", org.sonar.api.issue.Issue.STATUS_OPEN, new Date()));
Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
private final Integer line;
private final String message, lineHash;
private final String status;
+ private final Date creationDate;
- Issue(@Nullable Integer line, String lineHash, RuleKey ruleKey, String message, String status) {
+ Issue(@Nullable Integer line, String lineHash, RuleKey ruleKey, String message, String status, Date creationDate) {
this.line = line;
this.lineHash = lineHash;
this.ruleKey = ruleKey;
this.status = status;
+ this.creationDate = creationDate;
this.message = trim(message);
}
public String getStatus() {
return status;
}
+
+ @Override
+ public Date getCreationDate() {
+ return creationDate;
+ }
}
private static class FakeInput implements Input<Issue> {
}
Issue createIssueOnLine(int line, RuleKey ruleKey, String message) {
- Issue issue = new Issue(line, lineHashes.get(line - 1), ruleKey, message, org.sonar.api.issue.Issue.STATUS_OPEN);
+ Issue issue = new Issue(line, lineHashes.get(line - 1), ruleKey, message, org.sonar.api.issue.Issue.STATUS_OPEN, new Date());
issues.add(issue);
return issue;
}
* No line (line 0)
*/
Issue createIssue(RuleKey ruleKey, String message) {
- Issue issue = new Issue(null, "", ruleKey, message, org.sonar.api.issue.Issue.STATUS_OPEN);
+ Issue issue = new Issue(null, "", ruleKey, message, org.sonar.api.issue.Issue.STATUS_OPEN, new Date());
issues.add(issue);
return issue;
}
*/
package org.sonar.scanner.issue.tracking;
+import java.util.Date;
import javax.annotation.CheckForNull;
import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.DateUtils;
import org.sonar.core.issue.tracking.Trackable;
import static org.apache.commons.lang.StringUtils.trim;
@Override
public String getStatus() {
- throw new UnsupportedOperationException();
+ return dto.getStatus();
+ }
+
+ @Override
+ public Date getCreationDate() {
+ return DateUtils.longToDate(dto.getCreationDate());
}
}