Skip to content

Commit

Permalink
SONAR-11513 backdate issues on new files
Browse files Browse the repository at this point in the history
  • Loading branch information
sns-seb authored and SonarTech committed Dec 3, 2018
1 parent a11e761 commit e96e630
Show file tree
Hide file tree
Showing 9 changed files with 571 additions and 174 deletions.
Expand Up @@ -43,6 +43,7 @@
import org.sonar.ce.task.projectanalysis.duplication.DuplicationRepositoryImpl; import org.sonar.ce.task.projectanalysis.duplication.DuplicationRepositoryImpl;
import org.sonar.ce.task.projectanalysis.duplication.IntegrateCrossProjectDuplications; import org.sonar.ce.task.projectanalysis.duplication.IntegrateCrossProjectDuplications;
import org.sonar.ce.task.projectanalysis.event.EventRepositoryImpl; import org.sonar.ce.task.projectanalysis.event.EventRepositoryImpl;
import org.sonar.ce.task.projectanalysis.filemove.AddedFileRepositoryImpl;
import org.sonar.ce.task.projectanalysis.filemove.FileSimilarityImpl; import org.sonar.ce.task.projectanalysis.filemove.FileSimilarityImpl;
import org.sonar.ce.task.projectanalysis.filemove.MutableMovedFilesRepositoryImpl; import org.sonar.ce.task.projectanalysis.filemove.MutableMovedFilesRepositoryImpl;
import org.sonar.ce.task.projectanalysis.filemove.ScoreMatrixDumperImpl; import org.sonar.ce.task.projectanalysis.filemove.ScoreMatrixDumperImpl;
Expand Down Expand Up @@ -284,6 +285,7 @@ private static List<Object> componentClasses() {
SourceSimilarityImpl.class, SourceSimilarityImpl.class,
FileSimilarityImpl.class, FileSimilarityImpl.class,
MutableMovedFilesRepositoryImpl.class, MutableMovedFilesRepositoryImpl.class,
AddedFileRepositoryImpl.class,


// duplication // duplication
IntegrateCrossProjectDuplications.class, IntegrateCrossProjectDuplications.class,
Expand Down
@@ -0,0 +1,30 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.filemove;

import org.sonar.ce.task.projectanalysis.component.Component;

public interface AddedFileRepository {
/**
* @return {@code true} for any component on first analysis, otherwise {@code true} only if the specified component is
* a {@link Component.Type#FILE file} registered to the repository.
*/
boolean isAdded(Component component);
}
@@ -0,0 +1,60 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.filemove;

import java.util.HashSet;
import java.util.Set;
import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
import org.sonar.ce.task.projectanalysis.component.Component;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

public class AddedFileRepositoryImpl implements MutableAddedFileRepository {
private final Set<Component> addedComponents = new HashSet<>();
private final AnalysisMetadataHolder analysisMetadataHolder;

public AddedFileRepositoryImpl(AnalysisMetadataHolder analysisMetadataHolder) {
this.analysisMetadataHolder = analysisMetadataHolder;
}

@Override
public boolean isAdded(Component component) {
checkComponent(component);
if (analysisMetadataHolder.isFirstAnalysis()) {
return true;
}
return addedComponents.contains(component);
}

@Override
public void register(Component component) {
checkComponent(component);
checkArgument(component.getType() == Component.Type.FILE, "component must be a file");
checkState(!analysisMetadataHolder.isFirstAnalysis(), "No file can be registered on first analysis");

addedComponents.add(component);
}

private static void checkComponent(Component component) {
checkNotNull(component, "component can't be null");
}
}
Expand Up @@ -22,7 +22,6 @@
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import java.util.ArrayList; import java.util.ArrayList;
Expand Down Expand Up @@ -62,7 +61,7 @@
import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER; import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER;


public class FileMoveDetectionStep implements ComputationStep { public class FileMoveDetectionStep implements ComputationStep {
protected static final int MIN_REQUIRED_SCORE = 85; static final int MIN_REQUIRED_SCORE = 85;
private static final Logger LOG = Loggers.get(FileMoveDetectionStep.class); private static final Logger LOG = Loggers.get(FileMoveDetectionStep.class);
private static final Comparator<ScoreMatrix.ScoreFile> SCORE_FILE_COMPARATOR = (o1, o2) -> -1 * Integer.compare(o1.getLineCount(), o2.getLineCount()); private static final Comparator<ScoreMatrix.ScoreFile> SCORE_FILE_COMPARATOR = (o1, o2) -> -1 * Integer.compare(o1.getLineCount(), o2.getLineCount());
private static final double LOWER_BOUND_RATIO = 0.84; private static final double LOWER_BOUND_RATIO = 0.84;
Expand All @@ -75,17 +74,19 @@ public class FileMoveDetectionStep implements ComputationStep {
private final MutableMovedFilesRepository movedFilesRepository; private final MutableMovedFilesRepository movedFilesRepository;
private final SourceLinesHashRepository sourceLinesHash; private final SourceLinesHashRepository sourceLinesHash;
private final ScoreMatrixDumper scoreMatrixDumper; private final ScoreMatrixDumper scoreMatrixDumper;
private final MutableAddedFileRepository addedFileRepository;


public FileMoveDetectionStep(AnalysisMetadataHolder analysisMetadataHolder, TreeRootHolder rootHolder, DbClient dbClient, public FileMoveDetectionStep(AnalysisMetadataHolder analysisMetadataHolder, TreeRootHolder rootHolder, DbClient dbClient,
FileSimilarity fileSimilarity, MutableMovedFilesRepository movedFilesRepository, SourceLinesHashRepository sourceLinesHash, FileSimilarity fileSimilarity, MutableMovedFilesRepository movedFilesRepository, SourceLinesHashRepository sourceLinesHash,
ScoreMatrixDumper scoreMatrixDumper) { ScoreMatrixDumper scoreMatrixDumper, MutableAddedFileRepository addedFileRepository) {
this.analysisMetadataHolder = analysisMetadataHolder; this.analysisMetadataHolder = analysisMetadataHolder;
this.rootHolder = rootHolder; this.rootHolder = rootHolder;
this.dbClient = dbClient; this.dbClient = dbClient;
this.fileSimilarity = fileSimilarity; this.fileSimilarity = fileSimilarity;
this.movedFilesRepository = movedFilesRepository; this.movedFilesRepository = movedFilesRepository;
this.sourceLinesHash = sourceLinesHash; this.sourceLinesHash = sourceLinesHash;
this.scoreMatrixDumper = scoreMatrixDumper; this.scoreMatrixDumper = scoreMatrixDumper;
this.addedFileRepository = addedFileRepository;
} }


@Override @Override
Expand All @@ -103,25 +104,30 @@ public void execute(ComputationStep.Context context) {
Profiler p = Profiler.createIfTrace(LOG); Profiler p = Profiler.createIfTrace(LOG);


p.start(); p.start();
Map<String, DbComponent> dbFilesByKey = getDbFilesByKey();
context.getStatistics().add("dbFiles", dbFilesByKey.size());
if (dbFilesByKey.isEmpty()) {
LOG.debug("Previous snapshot has no file. Do nothing.");
return;
}

Map<String, Component> reportFilesByKey = getReportFilesByKey(this.rootHolder.getRoot()); Map<String, Component> reportFilesByKey = getReportFilesByKey(this.rootHolder.getRoot());
context.getStatistics().add("reportFiles", reportFilesByKey.size());
if (reportFilesByKey.isEmpty()) { if (reportFilesByKey.isEmpty()) {
LOG.debug("No files in report. Do nothing."); LOG.debug("No files in report. No file move detection.");
return; return;
} }


Set<String> addedFileKeys = ImmutableSet.copyOf(Sets.difference(reportFilesByKey.keySet(), dbFilesByKey.keySet())); Map<String, DbComponent> dbFilesByKey = getDbFilesByKey();
context.getStatistics().add("dbFiles", dbFilesByKey.size());

Set<String> addedFileKeys = difference(reportFilesByKey.keySet(), dbFilesByKey.keySet());
context.getStatistics().add("addedFiles", addedFileKeys.size()); context.getStatistics().add("addedFiles", addedFileKeys.size());
Set<String> removedFileKeys = ImmutableSet.copyOf(Sets.difference(dbFilesByKey.keySet(), reportFilesByKey.keySet()));
if (dbFilesByKey.isEmpty()) {
registerAddedFiles(addedFileKeys, reportFilesByKey, null);
LOG.debug("Previous snapshot has no file. No file move detection.");
return;
}

Set<String> removedFileKeys = difference(dbFilesByKey.keySet(), reportFilesByKey.keySet());


// can find matches if at least one of the added or removed files groups is empty => abort // can find matches if at least one of the added or removed files groups is empty => abort
if (addedFileKeys.isEmpty() || removedFileKeys.isEmpty()) { if (addedFileKeys.isEmpty() || removedFileKeys.isEmpty()) {
registerAddedFiles(addedFileKeys, reportFilesByKey, null);
LOG.debug("Either no files added or no files removed. Do nothing."); LOG.debug("Either no files added or no files removed. Do nothing.");
return; return;
} }
Expand All @@ -138,6 +144,8 @@ public void execute(ComputationStep.Context context) {


// not a single match with score higher than MIN_REQUIRED_SCORE => abort // not a single match with score higher than MIN_REQUIRED_SCORE => abort
if (scoreMatrix.getMaxScore() < MIN_REQUIRED_SCORE) { if (scoreMatrix.getMaxScore() < MIN_REQUIRED_SCORE) {
context.getStatistics().add("movedFiles", 0);
registerAddedFiles(addedFileKeys, reportFilesByKey, null);
LOG.debug("max score in matrix is less than min required score ({}). Do nothing.", MIN_REQUIRED_SCORE); LOG.debug("max score in matrix is less than min required score ({}). Do nothing.", MIN_REQUIRED_SCORE);
return; return;
} }
Expand All @@ -148,7 +156,16 @@ public void execute(ComputationStep.Context context) {
ElectedMatches electedMatches = electMatches(removedFileKeys, reportFileSourcesByKey, matchesByScore); ElectedMatches electedMatches = electMatches(removedFileKeys, reportFileSourcesByKey, matchesByScore);
p.stopTrace("Matches elected"); p.stopTrace("Matches elected");


context.getStatistics().add("movedFiles", electedMatches.size());
registerMatches(dbFilesByKey, reportFilesByKey, electedMatches); registerMatches(dbFilesByKey, reportFilesByKey, electedMatches);
registerAddedFiles(addedFileKeys, reportFilesByKey, electedMatches);
}

public Set<String> difference(Set<String> set1, Set<String> set2) {
if (set1.isEmpty() || set2.isEmpty()) {
return set1;
}
return Sets.difference(set1, set2).immutableCopy();
} }


private void registerMatches(Map<String, DbComponent> dbFilesByKey, Map<String, Component> reportFilesByKey, ElectedMatches electedMatches) { private void registerMatches(Map<String, DbComponent> dbFilesByKey, Map<String, Component> reportFilesByKey, ElectedMatches electedMatches) {
Expand All @@ -161,6 +178,22 @@ private void registerMatches(Map<String, DbComponent> dbFilesByKey, Map<String,
} }
} }


private void registerAddedFiles(Set<String> addedFileKeys, Map<String, Component> reportFilesByKey, @Nullable ElectedMatches electedMatches) {
if (electedMatches == null || electedMatches.isEmpty()) {
addedFileKeys.stream()
.map(reportFilesByKey::get)
.forEach(addedFileRepository::register);
} else {
Set<String> reallyAddedFileKeys = new HashSet<>(addedFileKeys);
for (Match electedMatch : electedMatches) {
reallyAddedFileKeys.remove(electedMatch.getReportKey());
}
reallyAddedFileKeys.stream()
.map(reportFilesByKey::get)
.forEach(addedFileRepository::register);
}
}

private Map<String, DbComponent> getDbFilesByKey() { private Map<String, DbComponent> getDbFilesByKey() {
try (DbSession dbSession = dbClient.openSession(false)) { try (DbSession dbSession = dbClient.openSession(false)) {
ImmutableList.Builder<DbComponent> builder = ImmutableList.builder(); ImmutableList.Builder<DbComponent> builder = ImmutableList.builder();
Expand Down Expand Up @@ -402,5 +435,9 @@ public Iterator<Match> iterator() {
public int size() { public int size() {
return matches.size(); return matches.size();
} }

public boolean isEmpty() {
return matches.isEmpty();
}
} }
} }
@@ -0,0 +1,32 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.filemove;

import org.sonar.ce.task.projectanalysis.component.Component;

public interface MutableAddedFileRepository extends AddedFileRepository {

/**
* @throws IllegalArgumentException if the specified component is not a {@link Component.Type#FILE File}
* @throws IllegalStateException on first analysis as all components are added on first analysis, none should be
* registered for performance reasons.
*/
void register(Component file);
}
Expand Up @@ -30,6 +30,7 @@
import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder; import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
import org.sonar.ce.task.projectanalysis.analysis.ScannerPlugin; import org.sonar.ce.task.projectanalysis.analysis.ScannerPlugin;
import org.sonar.ce.task.projectanalysis.component.Component; import org.sonar.ce.task.projectanalysis.component.Component;
import org.sonar.ce.task.projectanalysis.filemove.AddedFileRepository;
import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRule; import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRule;
import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolder; import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolder;
import org.sonar.ce.task.projectanalysis.scm.Changeset; import org.sonar.ce.task.projectanalysis.scm.Changeset;
Expand All @@ -53,39 +54,55 @@ public class IssueCreationDateCalculator extends IssueVisitor {
private final IssueChangeContext changeContext; private final IssueChangeContext changeContext;
private final ActiveRulesHolder activeRulesHolder; private final ActiveRulesHolder activeRulesHolder;
private final RuleRepository ruleRepository; private final RuleRepository ruleRepository;
private final AddedFileRepository addedFileRepository;


public IssueCreationDateCalculator(AnalysisMetadataHolder analysisMetadataHolder, ScmInfoRepository scmInfoRepository, public IssueCreationDateCalculator(AnalysisMetadataHolder analysisMetadataHolder, ScmInfoRepository scmInfoRepository,
IssueFieldsSetter issueUpdater, ActiveRulesHolder activeRulesHolder, RuleRepository ruleRepository) { IssueFieldsSetter issueUpdater, ActiveRulesHolder activeRulesHolder, RuleRepository ruleRepository,
AddedFileRepository addedFileRepository) {
this.scmInfoRepository = scmInfoRepository; this.scmInfoRepository = scmInfoRepository;
this.issueUpdater = issueUpdater; this.issueUpdater = issueUpdater;
this.analysisMetadataHolder = analysisMetadataHolder; this.analysisMetadataHolder = analysisMetadataHolder;
this.ruleRepository = ruleRepository; this.ruleRepository = ruleRepository;
this.changeContext = createScan(new Date(analysisMetadataHolder.getAnalysisDate())); this.changeContext = createScan(new Date(analysisMetadataHolder.getAnalysisDate()));
this.activeRulesHolder = activeRulesHolder; this.activeRulesHolder = activeRulesHolder;
this.addedFileRepository = addedFileRepository;
} }


@Override @Override
public void onIssue(Component component, DefaultIssue issue) { public void onIssue(Component component, DefaultIssue issue) {
if (!issue.isNew()) { if (!issue.isNew()) {
return; return;
} }

Optional<Long> lastAnalysisOptional = lastAnalysis(); Optional<Long> lastAnalysisOptional = lastAnalysis();
boolean firstAnalysis = !lastAnalysisOptional.isPresent(); boolean firstAnalysis = !lastAnalysisOptional.isPresent();
if (firstAnalysis || isNewFile(component)) {
backdateIssue(component, issue);
return;
}

Rule rule = ruleRepository.findByKey(issue.getRuleKey()) Rule rule = ruleRepository.findByKey(issue.getRuleKey())
.orElseThrow(illegalStateException("The rule with key '%s' raised an issue, but no rule with that key was found", issue.getRuleKey())); .orElseThrow(illegalStateException("The rule with key '%s' raised an issue, but no rule with that key was found", issue.getRuleKey()));

if (rule.isExternal()) { if (rule.isExternal()) {
getDateOfLatestChange(component, issue).ifPresent(changeDate -> updateDate(issue, changeDate)); backdateIssue(component, issue);
} else { } else {
// Rule can't be inactive (see contract of IssueVisitor) // Rule can't be inactive (see contract of IssueVisitor)
ActiveRule activeRule = activeRulesHolder.get(issue.getRuleKey()).get(); ActiveRule activeRule = activeRulesHolder.get(issue.getRuleKey()).get();
if (firstAnalysis || activeRuleIsNew(activeRule, lastAnalysisOptional.get()) if (activeRuleIsNew(activeRule, lastAnalysisOptional.get())
|| ruleImplementationChanged(activeRule.getRuleKey(), activeRule.getPluginKey(), lastAnalysisOptional.get())) { || ruleImplementationChanged(activeRule.getRuleKey(), activeRule.getPluginKey(), lastAnalysisOptional.get())) {
getDateOfLatestChange(component, issue).ifPresent(changeDate -> updateDate(issue, changeDate)); backdateIssue(component, issue);
} }
} }
} }


private boolean isNewFile(Component component) {
return component.getType() == Component.Type.FILE && addedFileRepository.isAdded(component);
}

private void backdateIssue(Component component, DefaultIssue issue) {
getDateOfLatestChange(component, issue).ifPresent(changeDate -> updateDate(issue, changeDate));
}

private boolean ruleImplementationChanged(RuleKey ruleKey, @Nullable String pluginKey, long lastAnalysisDate) { private boolean ruleImplementationChanged(RuleKey ruleKey, @Nullable String pluginKey, long lastAnalysisDate) {
if (pluginKey == null) { if (pluginKey == null) {
return false; return false;
Expand Down

0 comments on commit e96e630

Please sign in to comment.