+++ /dev/null
-/*
- * Sonar, open source software quality management tool.
- * Copyright (C) 2008-2012 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * Sonar 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.
- *
- * Sonar 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 Sonar; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
- */
-package org.sonar.plugins.core.timemachine;
-
-import org.sonar.api.database.model.RuleFailureModel;
-import org.sonar.api.rules.Violation;
-
-import java.util.Comparator;
-
-public class ViolationPair {
-
- private final RuleFailureModel pastViolation;
- private final Violation newViolation;
- private final int weight;
-
- public ViolationPair(RuleFailureModel pastViolation, Violation newViolation, int weight) {
- this.pastViolation = pastViolation;
- this.newViolation = newViolation;
- this.weight = weight;
- }
-
- public Violation getNewViolation() {
- return newViolation;
- }
-
- public RuleFailureModel getPastViolation() {
- return pastViolation;
- }
-
- public static final Comparator<ViolationPair> COMPARATOR = new Comparator<ViolationPair>() {
- public int compare(ViolationPair o1, ViolationPair o2) {
- return o2.weight - o1.weight;
- }
- };
-
-}
*/
package org.sonar.plugins.core.timemachine;
+import com.google.common.annotations.VisibleForTesting;
import org.sonar.plugins.core.timemachine.tracking.HashedSequence;
import org.sonar.plugins.core.timemachine.tracking.HashedSequenceComparator;
import org.sonar.plugins.core.timemachine.tracking.StringText;
private final HashedSequence<StringText> b;
private final HashedSequenceComparator<StringText> cmp;
+ @VisibleForTesting
public ViolationTrackingBlocksRecognizer(String referenceSource, String source) {
- this(new StringText(referenceSource), new StringText(source), StringTextComparator.IGNORE_WHITESPACE);
+ this.a = HashedSequence.wrap(new StringText(referenceSource), StringTextComparator.IGNORE_WHITESPACE);
+ this.b = HashedSequence.wrap(new StringText(source), StringTextComparator.IGNORE_WHITESPACE);
+ this.cmp = new HashedSequenceComparator<StringText>(StringTextComparator.IGNORE_WHITESPACE);
}
- private ViolationTrackingBlocksRecognizer(StringText a, StringText b, StringTextComparator cmp) {
- this.a = HashedSequence.wrap(a, cmp);
- this.b = HashedSequence.wrap(b, cmp);
- this.cmp = new HashedSequenceComparator<StringText>(cmp);
+ public ViolationTrackingBlocksRecognizer(HashedSequence<StringText> a, HashedSequence<StringText> b, HashedSequenceComparator<StringText> cmp) {
+ this.a = a;
+ this.b = b;
+ this.cmp = cmp;
}
- public boolean isValidLineInReference(int line) {
- return (0 <= line) && (line < a.length());
+ public boolean isValidLineInReference(Integer line) {
+ return (line != null) && (0 <= line - 1) && (line - 1 < a.length());
}
- public boolean isValidLineInSource(int line) {
- return (0 <= line) && (line < b.length());
+ public boolean isValidLineInSource(Integer line) {
+ return (line != null) && (0 <= line - 1) && (line - 1 < b.length());
}
/**
package org.sonar.plugins.core.timemachine;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.*;
+import com.google.common.base.Objects;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
-import org.sonar.api.batch.*;
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorBarriers;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependedUpon;
+import org.sonar.api.batch.DependsUpon;
+import org.sonar.api.batch.SonarIndex;
import org.sonar.api.database.model.RuleFailureModel;
import org.sonar.api.resources.Project;
import org.sonar.api.resources.Resource;
import org.sonar.api.rules.Violation;
import org.sonar.api.violations.ViolationQuery;
-
-import java.util.*;
+import org.sonar.plugins.core.timemachine.tracking.HashedSequence;
+import org.sonar.plugins.core.timemachine.tracking.HashedSequenceComparator;
+import org.sonar.plugins.core.timemachine.tracking.RollingHashSequence;
+import org.sonar.plugins.core.timemachine.tracking.RollingHashSequenceComparator;
+import org.sonar.plugins.core.timemachine.tracking.StringText;
+import org.sonar.plugins.core.timemachine.tracking.StringTextComparator;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
@DependsUpon({DecoratorBarriers.END_OF_VIOLATIONS_GENERATION, DecoratorBarriers.START_VIOLATION_TRACKING})
@DependedUpon(DecoratorBarriers.END_OF_VIOLATION_TRACKING)
// If each new violation matches an old one we can stop the matching mechanism
if (referenceViolationsMap.size() != newViolations.size()) {
-
- // SONAR-3072
- ViolationTrackingBlocksRecognizer rec = null;
if (source != null && resource != null) {
String referenceSource = referenceAnalysis.getSource(resource);
if (referenceSource != null) {
- rec = new ViolationTrackingBlocksRecognizer(referenceSource, source);
-
- List<ViolationPair> possiblePairs = Lists.newArrayList();
- for (Violation newViolation : newViolations) {
- if (newViolation.getLineId() != null && rec.isValidLineInSource(newViolation.getLineId() - 1)) {
- for (RuleFailureModel pastViolation : pastViolationsByRule.get(newViolation.getRule().getId())) {
- if (pastViolation.getLine() != null && rec.isValidLineInReference(pastViolation.getLine() - 1)) {
- int weight = rec.computeLengthOfMaximalBlock(pastViolation.getLine() - 1, newViolation.getLineId() - 1);
- possiblePairs.add(new ViolationPair(pastViolation, newViolation, weight));
- }
- }
+ HashedSequence<StringText> hashedReference = HashedSequence.wrap(new StringText(referenceSource), StringTextComparator.IGNORE_WHITESPACE);
+ HashedSequence<StringText> hashedSource = HashedSequence.wrap(new StringText(source), StringTextComparator.IGNORE_WHITESPACE);
+ HashedSequenceComparator<StringText> hashedComparator = new HashedSequenceComparator<StringText>(StringTextComparator.IGNORE_WHITESPACE);
+
+ ViolationTrackingBlocksRecognizer rec = new ViolationTrackingBlocksRecognizer(hashedReference, hashedSource, hashedComparator);
+
+ Multimap<Integer, Violation> newViolationsByLines = newViolationsByLines(newViolations, rec);
+ Multimap<Integer, RuleFailureModel> pastViolationsByLines = pastViolationsByLines(pastViolations, rec);
+
+ RollingHashSequence<HashedSequence<StringText>> a = RollingHashSequence.wrap(hashedReference, hashedComparator, 5);
+ RollingHashSequence<HashedSequence<StringText>> b = RollingHashSequence.wrap(hashedSource, hashedComparator, 5);
+ RollingHashSequenceComparator<HashedSequence<StringText>> cmp = new RollingHashSequenceComparator<HashedSequence<StringText>>(hashedComparator);
+
+ Map<Integer, HashOccurrence> map = Maps.newHashMap();
+
+ for (Integer line : pastViolationsByLines.keySet()) {
+ int hash = cmp.hash(a, line - 1);
+ HashOccurrence hashOccurrence = map.get(hash);
+ if (hashOccurrence == null) {
+ // first occurrence in A
+ hashOccurrence = new HashOccurrence();
+ hashOccurrence.lineA = line;
+ hashOccurrence.countA = 1;
+ map.put(hash, hashOccurrence);
+ } else {
+ hashOccurrence.countA++;
}
}
- Collections.sort(possiblePairs, ViolationPair.COMPARATOR);
-
- Set<RuleFailureModel> pp = Sets.newHashSet(pastViolations);
- for (ViolationPair pair : possiblePairs) {
- Violation newViolation = pair.getNewViolation();
- RuleFailureModel pastViolation = pair.getPastViolation();
- if (isNotAlreadyMapped(newViolation, referenceViolationsMap) && pp.contains(pastViolation)) {
- pp.remove(pastViolation);
- mapViolation(newViolation, pastViolation, pastViolationsByRule, referenceViolationsMap);
+
+ for (Integer line : newViolationsByLines.keySet()) {
+ int hash = cmp.hash(b, line - 1);
+ HashOccurrence hashOccurrence = map.get(hash);
+ if (hashOccurrence != null) {
+ hashOccurrence.lineB = line;
+ hashOccurrence.countB++;
+ }
+ }
+
+ Set<RuleFailureModel> unmappedPastViolations = Sets.newHashSet(pastViolations);
+
+ for (HashOccurrence hashOccurrence : map.values()) {
+ if (hashOccurrence.countA == 1 && hashOccurrence.countB == 1) {
+ // Guaranteed that lineA has been moved to lineB, so we can map all violations on lineA to all violations on lineB
+ map(newViolationsByLines.get(hashOccurrence.lineB), pastViolationsByLines.get(hashOccurrence.lineA), unmappedPastViolations, pastViolationsByRule);
+ pastViolationsByLines.removeAll(hashOccurrence.lineA);
+ newViolationsByLines.removeAll(hashOccurrence.lineB);
+ }
+ }
+
+ // Check if remaining number of lines exceeds threshold
+ if (pastViolationsByLines.keySet().size() * newViolationsByLines.keySet().size() < 250000) {
+ List<LinePair> possibleLinePairs = Lists.newArrayList();
+ for (Integer oldLine : pastViolationsByLines.keySet()) {
+ for (Integer newLine : newViolationsByLines.keySet()) {
+ int weight = rec.computeLengthOfMaximalBlock(oldLine - 1, newLine - 1);
+ possibleLinePairs.add(new LinePair(oldLine, newLine, weight));
+ }
+ }
+ Collections.sort(possibleLinePairs, LINE_PAIR_COMPARATOR);
+ for (LinePair linePair : possibleLinePairs) {
+ // High probability that lineA has been moved to lineB, so we can map all violations on lineA to all violations on lineB
+ map(newViolationsByLines.get(linePair.lineB), pastViolationsByLines.get(linePair.lineA), unmappedPastViolations, pastViolationsByRule);
}
}
}
return referenceViolationsMap;
}
+ private void map(Collection<Violation> newViolations, Collection<RuleFailureModel> pastViolations, Set<RuleFailureModel> unmappedPastViolations,
+ Multimap<Integer, RuleFailureModel> pastViolationsByRule) {
+ for (Violation newViolation : newViolations) {
+ if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
+ for (RuleFailureModel pastViolation : pastViolations) {
+ if (unmappedPastViolations.contains(pastViolation) && Objects.equal(newViolation.getRule().getId(), pastViolation.getRuleId())) {
+ unmappedPastViolations.remove(pastViolation);
+ mapViolation(newViolation, pastViolation, pastViolationsByRule, referenceViolationsMap);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private Multimap<Integer, Violation> newViolationsByLines(List<Violation> newViolations, ViolationTrackingBlocksRecognizer rec) {
+ Multimap<Integer, Violation> newViolationsByLines = LinkedHashMultimap.create();
+ for (Violation newViolation : newViolations) {
+ if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
+ if (rec.isValidLineInSource(newViolation.getLineId())) {
+ newViolationsByLines.put(newViolation.getLineId(), newViolation);
+ }
+ }
+ }
+ return newViolationsByLines;
+ }
+
+ private Multimap<Integer, RuleFailureModel> pastViolationsByLines(List<RuleFailureModel> pastViolations, ViolationTrackingBlocksRecognizer rec) {
+ Multimap<Integer, RuleFailureModel> pastViolationsByLines = LinkedHashMultimap.create();
+ for (RuleFailureModel pastViolation : pastViolations) {
+ if (rec.isValidLineInSource(pastViolation.getLine())) {
+ pastViolationsByLines.put(pastViolation.getLine(), pastViolation);
+ }
+ }
+ return pastViolationsByLines;
+ }
+
+ private static final Comparator<LinePair> LINE_PAIR_COMPARATOR = new Comparator<LinePair>() {
+ public int compare(LinePair o1, LinePair o2) {
+ return o2.weight - o1.weight;
+ }
+ };
+
+ static class LinePair {
+ int lineA;
+ int lineB;
+ int weight;
+
+ public LinePair(int lineA, int lineB, int weight) {
+ this.lineA = lineA;
+ this.lineB = lineB;
+ this.weight = weight;
+ }
+ }
+
+ static class HashOccurrence {
+ int lineA;
+ int lineB;
+ int countA;
+ int countB;
+ }
+
private boolean isNotAlreadyMapped(Violation newViolation, Map<Violation, RuleFailureModel> violationMap) {
return !violationMap.containsKey(newViolation);
}
--- /dev/null
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package org.sonar.plugins.core.timemachine.tracking;
+
+/**
+ * Wraps a {@link Sequence} to assign hash codes to elements.
+ */
+public class RollingHashSequence<S extends Sequence> implements Sequence {
+
+ final S base;
+ final int[] hashes;
+
+ public static <S extends Sequence> RollingHashSequence<S> wrap(S base, SequenceComparator<S> cmp, int lines) {
+ int size = base.length();
+ int[] hashes = new int[size];
+
+ RollingHashCalculator hashCalulator = new RollingHashCalculator(lines * 2 + 1);
+ for (int i = 0; i <= Math.min(size - 1, lines); i++) {
+ hashCalulator.add(cmp.hash(base, i));
+ }
+ for (int i = 0; i < size; i++) {
+ hashes[i] = hashCalulator.getHash();
+ if (i - lines >= 0) {
+ hashCalulator.remove(cmp.hash(base, i - lines));
+ }
+ if (i + lines + 1 < size) {
+ hashCalulator.add(cmp.hash(base, i + lines + 1));
+ } else {
+ hashCalulator.add(0);
+ }
+ }
+
+ return new RollingHashSequence<S>(base, hashes);
+ }
+
+ private RollingHashSequence(S base, int[] hashes) {
+ this.base = base;
+ this.hashes = hashes;
+ }
+
+ public int length() {
+ return base.length();
+ }
+
+ private static class RollingHashCalculator {
+
+ private static final int PRIME_BASE = 31;
+
+ private final int power;
+ private int hash;
+
+ public RollingHashCalculator(int size) {
+ int pow = 1;
+ for (int i = 0; i < size - 1; i++) {
+ pow = pow * PRIME_BASE;
+ }
+ this.power = pow;
+ }
+
+ public void add(int value) {
+ hash = hash * PRIME_BASE + value;
+ }
+
+ public void remove(int value) {
+ hash = hash - power * value;
+ }
+
+ public int getHash() {
+ return hash;
+ }
+
+ }
+
+}
--- /dev/null
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package org.sonar.plugins.core.timemachine.tracking;
+
+/**
+ * Wrap another {@link SequenceComparator} for use with {@link RollingHashSequence}.
+ */
+public class RollingHashSequenceComparator<S extends Sequence> implements SequenceComparator<RollingHashSequence<S>> {
+
+ private final SequenceComparator<? super S> cmp;
+
+ public RollingHashSequenceComparator(SequenceComparator<? super S> cmp) {
+ this.cmp = cmp;
+ }
+
+ public boolean equals(RollingHashSequence<S> a, int ai, RollingHashSequence<S> b, int bi) {
+ if (a.hashes[ai] == b.hashes[bi]) {
+ return cmp.equals(a.base, ai, b.base, bi);
+ }
+ return false;
+ }
+
+ public int hash(RollingHashSequence<S> seq, int i) {
+ return seq.hashes[i];
+ }
+
+}
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
-@ParametersAreNonnullByDefault
+@javax.annotation.ParametersAreNonnullByDefault
package org.sonar.plugins.core.timemachine.tracking;
-import javax.annotation.ParametersAreNonnullByDefault;
-
--- /dev/null
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package org.sonar.plugins.core.timemachine.tracking;
+
+import org.junit.Test;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class RollingHashSequenceTest {
+
+ @Test
+ public void test_hash() {
+ StringText seq = new StringText("line0 \n line1 \n line2");
+ StringTextComparator cmp = StringTextComparator.IGNORE_WHITESPACE;
+ RollingHashSequence<StringText> seq2 = RollingHashSequence.wrap(seq, cmp, 1);
+ RollingHashSequenceComparator<StringText> cmp2 = new RollingHashSequenceComparator<StringText>(cmp);
+
+ assertThat(seq2.length()).isEqualTo(3);
+ assertThat(cmp2.hash(seq2, 0)).isEqualTo(cmp.hash(seq, 0) * 31 + cmp.hash(seq, 1));
+ assertThat(cmp2.hash(seq2, 1)).isEqualTo((cmp.hash(seq, 0) * 31 + cmp.hash(seq, 1)) * 31 + cmp.hash(seq, 2));
+ assertThat(cmp2.hash(seq2, 2)).isEqualTo((cmp.hash(seq, 1) * 31 + cmp.hash(seq, 2)) * 31);
+ }
+
+ @Test
+ public void test_equals() {
+ StringTextComparator baseCmp = StringTextComparator.IGNORE_WHITESPACE;
+ RollingHashSequence<StringText> a = RollingHashSequence.wrap(new StringText("line0 \n line1 \n line2"), baseCmp, 1);
+ RollingHashSequence<StringText> b = RollingHashSequence.wrap(new StringText("line0 \n line1 \n line2 \n line3"), baseCmp, 1);
+ RollingHashSequenceComparator<StringText> cmp = new RollingHashSequenceComparator<StringText>(baseCmp);
+
+ assertThat(cmp.equals(a, 0, b, 0)).isTrue();
+ assertThat(cmp.equals(a, 1, b, 1)).isTrue();
+ assertThat(cmp.equals(a, 2, b, 2)).isFalse();
+ }
+
+}