/* * SonarQube * Copyright (C) 2009-2017 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.scanner.issue.tracking; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.batch.ScannerSide; import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.InputFile.Status; import org.sonar.api.batch.fs.InputModule; import org.sonar.api.batch.fs.internal.DefaultInputFile; import org.sonar.api.batch.fs.internal.DefaultInputModule; import org.sonar.api.batch.fs.internal.InputComponentTree; import org.sonar.api.batch.rule.ActiveRule; import org.sonar.api.batch.rule.ActiveRules; import org.sonar.core.issue.tracking.Input; import org.sonar.core.issue.tracking.Tracker; import org.sonar.core.issue.tracking.Tracking; import org.sonar.scanner.analysis.DefaultAnalysisMode; import org.sonar.scanner.issue.IssueTransformer; import org.sonar.scanner.protocol.output.ScannerReport; import org.sonar.scanner.repository.ProjectRepositories; @ScannerSide public class LocalIssueTracking { private final Tracker<TrackedIssue, ServerIssueFromWs> tracker; private final ServerLineHashesLoader lastLineHashes; private final ActiveRules activeRules; private final ServerIssueRepository serverIssueRepository; private final DefaultAnalysisMode mode; private final InputComponentTree componentTree; private boolean hasServerAnalysis; public LocalIssueTracking(Tracker<TrackedIssue, ServerIssueFromWs> tracker, ServerLineHashesLoader lastLineHashes, InputComponentTree componentTree, ActiveRules activeRules, ServerIssueRepository serverIssueRepository, ProjectRepositories projectRepositories, DefaultAnalysisMode mode) { this.tracker = tracker; this.lastLineHashes = lastLineHashes; this.componentTree = componentTree; this.serverIssueRepository = serverIssueRepository; this.mode = mode; this.activeRules = activeRules; this.hasServerAnalysis = projectRepositories.lastAnalysisDate() != null; } public void init() { if (hasServerAnalysis) { serverIssueRepository.load(); } } public List<TrackedIssue> trackIssues(InputComponent component, Collection<ScannerReport.Issue> reportIssues, Date analysisDate) { List<TrackedIssue> trackedIssues = new LinkedList<>(); if (hasServerAnalysis) { // all the issues that are not closed in db before starting this module scan, including manual issues Collection<ServerIssueFromWs> serverIssues = loadServerIssues(component); if (shouldCopyServerIssues(component)) { // raw issues should be empty, we just need to deal with server issues (SONAR-6931) copyServerIssues(serverIssues, trackedIssues, component.key()); } else { SourceHashHolder sourceHashHolder = loadSourceHashes(component); Collection<TrackedIssue> rIssues = IssueTransformer.toTrackedIssue(component, reportIssues, sourceHashHolder); Input<ServerIssueFromWs> baseIssues = createBaseInput(serverIssues, sourceHashHolder); Input<TrackedIssue> rawIssues = createRawInput(rIssues, sourceHashHolder); Tracking<TrackedIssue, ServerIssueFromWs> track = tracker.track(rawIssues, baseIssues); addUnmatchedFromServer(track.getUnmatchedBases(), trackedIssues, component.key()); mergeMatched(track, trackedIssues, rIssues); addUnmatchedFromReport(track.getUnmatchedRaws(), trackedIssues, analysisDate); } } if (hasServerAnalysis && componentTree.getParent(component) == null) { Preconditions.checkState(component instanceof InputModule, "Object without parent is of type: " + component.getClass()); // issues that relate to deleted components addIssuesOnDeletedComponents(trackedIssues, component.key()); } return trackedIssues; } private static Input<ServerIssueFromWs> createBaseInput(Collection<ServerIssueFromWs> serverIssues, @Nullable SourceHashHolder sourceHashHolder) { List<String> refHashes; if (sourceHashHolder != null && sourceHashHolder.getHashedReference() != null) { refHashes = Arrays.asList(sourceHashHolder.getHashedReference().hashes()); } else { refHashes = new ArrayList<>(0); } return new IssueTrackingInput<>(serverIssues, refHashes); } private static Input<TrackedIssue> createRawInput(Collection<TrackedIssue> rIssues, @Nullable SourceHashHolder sourceHashHolder) { List<String> baseHashes; if (sourceHashHolder != null && sourceHashHolder.getHashedSource() != null) { baseHashes = Arrays.asList(sourceHashHolder.getHashedSource().hashes()); } else { baseHashes = new ArrayList<>(0); } return new IssueTrackingInput<>(rIssues, baseHashes); } private boolean shouldCopyServerIssues(InputComponent component) { if (!mode.scanAllFiles() && component.isFile()) { InputFile inputFile = (InputFile) component; if (inputFile.status() == Status.SAME) { return true; } } return false; } private void copyServerIssues(Collection<ServerIssueFromWs> serverIssues, List<TrackedIssue> trackedIssues, String componentKey) { for (ServerIssueFromWs serverIssue : serverIssues) { org.sonar.scanner.protocol.input.ScannerInput.ServerIssue unmatchedPreviousIssue = serverIssue.getDto(); TrackedIssue unmatched = IssueTransformer.toTrackedIssue(unmatchedPreviousIssue, componentKey); ActiveRule activeRule = activeRules.find(unmatched.getRuleKey()); unmatched.setNew(false); if (activeRule == null) { // rule removed IssueTransformer.resolveRemove(unmatched); } trackedIssues.add(unmatched); } } @CheckForNull private SourceHashHolder loadSourceHashes(InputComponent component) { SourceHashHolder sourceHashHolder = null; if (component.isFile()) { DefaultInputModule module = (DefaultInputModule) componentTree.getParent(componentTree.getParent(component)); DefaultInputFile file = (DefaultInputFile) component; sourceHashHolder = new SourceHashHolder(module, file, lastLineHashes); } return sourceHashHolder; } private Collection<ServerIssueFromWs> loadServerIssues(InputComponent component) { Collection<ServerIssueFromWs> serverIssues = new ArrayList<>(); for (org.sonar.scanner.protocol.input.ScannerInput.ServerIssue previousIssue : serverIssueRepository.byComponent(component)) { serverIssues.add(new ServerIssueFromWs(previousIssue)); } return serverIssues; } @VisibleForTesting protected void mergeMatched(Tracking<TrackedIssue, ServerIssueFromWs> result, Collection<TrackedIssue> mergeTo, Collection<TrackedIssue> rawIssues) { for (Map.Entry<TrackedIssue, ServerIssueFromWs> e : result.getMatchedRaws().entrySet()) { org.sonar.scanner.protocol.input.ScannerInput.ServerIssue dto = e.getValue().getDto(); TrackedIssue tracked = e.getKey(); // invariant fields tracked.setKey(dto.getKey()); // non-persisted fields tracked.setNew(false); // fields to update with old values tracked.setResolution(dto.hasResolution() ? dto.getResolution() : null); tracked.setStatus(dto.getStatus()); tracked.setAssignee(dto.hasAssigneeLogin() ? dto.getAssigneeLogin() : null); tracked.setCreationDate(new Date(dto.getCreationDate())); if (dto.getManualSeverity()) { // Severity overriden by user tracked.setSeverity(dto.getSeverity().name()); } mergeTo.add(tracked); } } private void addUnmatchedFromServer(Iterable<ServerIssueFromWs> unmatchedIssues, Collection<TrackedIssue> mergeTo, String componentKey) { for (ServerIssueFromWs unmatchedIssue : unmatchedIssues) { org.sonar.scanner.protocol.input.ScannerInput.ServerIssue unmatchedPreviousIssue = unmatchedIssue.getDto(); TrackedIssue unmatched = IssueTransformer.toTrackedIssue(unmatchedPreviousIssue, componentKey); updateUnmatchedIssue(unmatched); mergeTo.add(unmatched); } } private static void addUnmatchedFromReport(Iterable<TrackedIssue> rawIssues, Collection<TrackedIssue> trackedIssues, Date analysisDate) { for (TrackedIssue rawIssue : rawIssues) { rawIssue.setCreationDate(analysisDate); trackedIssues.add(rawIssue); } } private void addIssuesOnDeletedComponents(Collection<TrackedIssue> issues, String componentKey) { for (org.sonar.scanner.protocol.input.ScannerInput.ServerIssue previous : serverIssueRepository.issuesOnMissingComponents()) { TrackedIssue dead = IssueTransformer.toTrackedIssue(previous, componentKey); updateUnmatchedIssue(dead); issues.add(dead); } } private void updateUnmatchedIssue(TrackedIssue issue) { ActiveRule activeRule = activeRules.find(issue.getRuleKey()); issue.setNew(false); boolean isRemovedRule = activeRule == null; if (isRemovedRule) { IssueTransformer.resolveRemove(issue); } else { IssueTransformer.close(issue); } } }