diff --git a/ArchitectureReloaded.iml b/ArchitectureReloaded.iml
index ba88c60a..2dc730e1 100644
--- a/ArchitectureReloaded.iml
+++ b/ArchitectureReloaded.iml
@@ -1,12 +1,360 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 62ae2021..1d27113f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -24,5 +24,5 @@ dependencies {
compile project(':openapi')
compile project(':utils')
compile project(':stockmetrics')
- compile files('lib/args4j-2.32.jar', 'lib/jcommon-0.9.1.jar', 'lib/jfreechart-0.9.16.jar')
+ compile files('lib/args4j-2.32.jar', 'lib/jcommon-0.9.1.jar', 'lib/jfreechart-0.9.16.jar', 'lib/PorterStemmer.jar')
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 0b6241a9..6a9fb70e 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Wed Jul 12 12:29:38 MSK 2017
+#Mon Jul 09 12:44:35 MSK 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
diff --git a/openapi/openapi.iml b/openapi/openapi.iml
index 842a877d..377997b8 100644
--- a/openapi/openapi.iml
+++ b/openapi/openapi.iml
@@ -1,15 +1,9 @@
-
-
-
-
+
+
-
-
-
-
+
-
-
+
\ No newline at end of file
diff --git a/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/OldAlgorithm.java b/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/OldAlgorithm.java
index 5a174926..815da057 100644
--- a/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/OldAlgorithm.java
+++ b/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/OldAlgorithm.java
@@ -136,7 +136,7 @@ private void setFields(OldEntity entity, ElementAttributes attributes) {
vectorField.set(entity, attributes.getRawFeatures());
vectorField.setAccessible(accessible);
- } catch (NoSuchFieldException | IllegalAccessException e) {
+ } catch (NoSuchFieldException | IllegalAccessException ignored) {
}
}
}
diff --git a/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/RMMR.java b/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/RMMR.java
new file mode 100644
index 00000000..e3301956
--- /dev/null
+++ b/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/RMMR.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2018 Machine Learning Methods in Software Engineering Group of JetBrains Research
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jetbrains.research.groups.ml_methods.algorithm;
+
+import com.sixrr.metrics.Metric;
+import org.apache.log4j.Logger;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.research.groups.ml_methods.algorithm.entity.ClassOldEntity;
+import org.jetbrains.research.groups.ml_methods.algorithm.entity.EntitySearchResult;
+import org.jetbrains.research.groups.ml_methods.algorithm.entity.MethodOldEntity;
+import org.jetbrains.research.groups.ml_methods.algorithm.refactoring.Refactoring;
+import org.jetbrains.research.groups.ml_methods.config.Logging;
+import org.jetbrains.research.groups.ml_methods.utils.AlgorithmsUtil;
+
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+import static java.lang.Math.*;
+
+/**
+ * Implementation of RMMR (Recommendation of Move Method Refactoring) algorithm.
+ * Based on @see this article.
+ */
+// TODO: maybe consider that method and target class are in different packages?
+public class RMMR extends OldAlgorithm {
+ /** Internal name of the algorithm in the program */
+ public static final String NAME = "RMMR";
+ private static final boolean ENABLE_PARALLEL_EXECUTION = false;
+ /** Describes minimal accuracy that algorithm accepts */
+ private final static double MIN_ACCURACY = 0.01;
+ /** Describes accuracy that is pretty confident to do refactoring */
+ private final static double GOOD_ACCURACY_BOUND = 0.5;
+ /** Describes accuracy higher which accuracy considered as max = 1 */
+ private final static double MAX_ACCURACY_BOUND = 0.7;
+ /** Describes power to which stretched accuracy will be raised */
+ // value is to get this result: GOOD_ACCURACY_BOUND go to MAX_ACCURACY_BOUND
+ private final static double POWER_FOR_ACCURACY = log(MAX_ACCURACY_BOUND) / log(GOOD_ACCURACY_BOUND / MAX_ACCURACY_BOUND);
+ private static final Logger LOGGER = Logging.getLogger(RMMR.class);
+ private final Map> methodsByClass = new HashMap<>();
+ private final List units = new ArrayList<>();
+ /** Classes to which method will be considered for moving */
+ private final List classEntities = new ArrayList<>();
+ private final AtomicInteger progressCount = new AtomicInteger();
+ /** Context which stores all found classes, methods and its metrics (by storing OldEntity) */
+ private OldExecutionContext context;
+
+ public RMMR() {
+ super(NAME, true);
+ }
+
+ @Override
+ @NotNull
+ protected List calculateRefactorings(@NotNull OldExecutionContext context, boolean enableFieldRefactorings) {
+ if (enableFieldRefactorings) {
+ LOGGER.error("Field refactorings are not supported",
+ new UnsupportedOperationException("Field refactorings are not supported"));
+ }
+ this.context = context;
+ init();
+
+ if (ENABLE_PARALLEL_EXECUTION) {
+ return context.runParallel(units, ArrayList::new, this::findRefactoring, AlgorithmsUtil::combineLists);
+ } else {
+ List accum = new LinkedList<>();
+ units.forEach(methodEntity -> findRefactoring(methodEntity, accum));
+ return accum;
+ }
+ }
+
+ /** Initializes units, methodsByClass, classEntities. Data is gathered from context.getEntities() */
+ private void init() {
+ final EntitySearchResult entities = context.getEntities();
+ LOGGER.info("Init RMMR");
+ units.clear();
+ classEntities.clear();
+ methodsByClass.clear();
+
+ classEntities.addAll(entities.getClasses());
+ units.addAll(entities.getMethods());
+ progressCount.set(0);
+
+ entities.getMethods().forEach(methodEntity -> {
+ List methodClassEntity = entities.getClasses().stream()
+ .filter(classEntity -> methodEntity.getClassName().equals(classEntity.getName()))
+ .collect(Collectors.toList());
+ if (methodClassEntity.size() != 1) {
+ LOGGER.error("Found more than 1 class that has this method");
+ }
+ methodsByClass.computeIfAbsent(methodClassEntity.get(0), anyKey -> new HashSet<>()).add(methodEntity);
+ });
+ }
+
+ /**
+ * Methods decides whether to move method or not, based on calculating distances between given method and classes.
+ *
+ * @param entity method to check for move method refactoring.
+ * @param accumulator list of refactorings, if method must be moved, refactoring for it will be added to this accumulator.
+ * @return changed or unchanged accumulator.
+ */
+ @NotNull
+ private List findRefactoring(@NotNull MethodOldEntity entity, @NotNull List accumulator) {
+ context.reportProgress((double) progressCount.incrementAndGet() / units.size());
+ context.checkCanceled();
+ if (!entity.isMovable() || classEntities.size() < 2) {
+ return accumulator;
+ }
+ double minDistance = Double.POSITIVE_INFINITY;
+ double difference = Double.POSITIVE_INFINITY;
+ double distanceWithSourceClass = 1;
+ ClassOldEntity targetClass = null;
+ ClassOldEntity sourceClass = null;
+ for (final ClassOldEntity classEntity : classEntities) {
+ final double contextualDistance = classEntity.getRelevantProperties().getContextualVector().size() == 0 ? 1 : getContextualDistance(entity, classEntity);
+ final double conceptualDistance = getConceptualDistance(entity, classEntity);
+ final double distance = 0.55 * conceptualDistance + 0.45 * contextualDistance;
+ if (classEntity.getName().equals(entity.getClassName())) {
+ sourceClass = classEntity;
+ distanceWithSourceClass = distance;
+ }
+ if (distance < minDistance) {
+ difference = minDistance - distance;
+ minDistance = distance;
+ targetClass = classEntity;
+ } else if (distance - minDistance < difference) {
+ difference = distance - minDistance;
+ }
+ }
+
+ if (targetClass == null) {
+ LOGGER.warn("targetClass is null for " + entity.getName());
+ return accumulator;
+ }
+ final String targetClassName = targetClass.getName();
+ double differenceWithSourceClass = distanceWithSourceClass - minDistance;
+ int numberOfMethodsInSourceClass = methodsByClass.get(sourceClass).size();
+ int numberOfMethodsInTargetClass = methodsByClass.getOrDefault(targetClass, Collections.emptySet()).size();
+ // considers amount of entities.
+ double sourceClassCoefficient = min(1, max(1.1 - 1.0 / (2 * numberOfMethodsInSourceClass * numberOfMethodsInSourceClass), 0));
+ double targetClassCoefficient = min(1, max(1.1 - 1.0 / (4 * numberOfMethodsInTargetClass * numberOfMethodsInTargetClass), 0));
+ double powerCoefficient = min(1, max(1.1 - 1.0 / (2 * entity.getRelevantProperties().getClasses().size()), 0));
+ double accuracy = (0.5 * distanceWithSourceClass + 0.1 * (1 - minDistance) + 0.4 * differenceWithSourceClass) * powerCoefficient * sourceClassCoefficient * targetClassCoefficient;
+ if (entity.getClassName().contains("Util") || entity.getClassName().contains("Factory") ||
+ entity.getClassName().contains("Builder")) {
+ if (accuracy > GOOD_ACCURACY_BOUND) {
+ accuracy /= 2;
+ }
+ }
+ if (entity.getName().contains("main")) {
+ accuracy /= 2;
+ }
+ accuracy = min(pow(accuracy / MAX_ACCURACY_BOUND, POWER_FOR_ACCURACY), 1);
+ if (differenceWithSourceClass != 0 && accuracy >= MIN_ACCURACY && !targetClassName.equals(entity.getClassName())) {
+ accumulator.add(Refactoring.createRefactoring(entity.getName(), targetClassName, accuracy, entity.isField(), context.getScope()));
+ }
+ return accumulator;
+ }
+
+ /**
+ * Measures contextual distance (a number in [0; 1]) between method and a class.
+ * It is cosine between two contextual vectors.
+ * If there is a null vector then cosine is 1.
+ *
+ * @param methodEntity method to calculate contextual distance.
+ * @param classEntity class to calculate contextual distance.
+ * @return contextual distance between the method and the class.
+ */
+ private double getContextualDistance(@NotNull MethodOldEntity methodEntity, @NotNull ClassOldEntity classEntity) {
+ Map methodVector = methodEntity.getRelevantProperties().getContextualVector();
+ Map classVector = classEntity.getRelevantProperties().getContextualVector();
+ double methodVectorNorm = norm(methodVector);
+ double classVectorNorm = norm(classVector);
+ return methodVectorNorm == 0 || classVectorNorm == 0 ?
+ 1 : 1 - dotProduct(methodVector, classVector) / (methodVectorNorm * classVectorNorm);
+ }
+
+ private double dotProduct(@NotNull Map vector1, @NotNull Map vector2) {
+ final double[] productValue = {0};
+ vector1.forEach((s, aDouble) -> productValue[0] += aDouble * vector2.getOrDefault(s, 0.0));
+ return productValue[0];
+ }
+
+ private double norm(@NotNull Map vector) {
+ return sqrt(dotProduct(vector, vector));
+ }
+
+ /**
+ * Measures conceptual distance (a number in [0; 1]) between method and a class.
+ * It is an average of distances between method and class methods.
+ * If there is no methods in a given class then distance is 1.
+ * @param methodEntity method to calculate conceptual distance.
+ * @param classEntity class to calculate conceptual distance.
+ * @return conceptual distance between the method and the class.
+ */
+ private double getConceptualDistance(@NotNull MethodOldEntity methodEntity, @NotNull ClassOldEntity classEntity) {
+ int number = 0;
+ double sumOfDistances = 0;
+
+ if (methodsByClass.containsKey(classEntity)) {
+ for (MethodOldEntity methodEntityInClass : methodsByClass.get(classEntity)) {
+ if (!methodEntity.equals(methodEntityInClass)) {
+ sumOfDistances += getConceptualDistance(methodEntity, methodEntityInClass);
+ number++;
+ }
+ }
+ }
+
+ return number == 0 ? 1 : sumOfDistances / number;
+ }
+
+ /**
+ * Measures conceptual distance (a number in [0; 1]) between two methods.
+ * It is sizeOfIntersection(A1, A2) / sizeOfUnion(A1, A2), where Ai is a conceptual set of method.
+ * If A1 and A2 are empty then distance is 1.
+ * @param methodEntity1 method to calculate conceptual distance.
+ * @param methodEntity2 method to calculate conceptual distance.
+ * @return conceptual distance between two given methods.
+ */
+ private double getConceptualDistance(@NotNull MethodOldEntity methodEntity1, @NotNull MethodOldEntity methodEntity2) {
+ Set method1Classes = methodEntity1.getRelevantProperties().getClasses();
+ Set method2Classes = methodEntity2.getRelevantProperties().getClasses();
+ int sizeOfIntersection = intersection(method1Classes, method2Classes).size();
+ int sizeOfUnion = union(method1Classes, method2Classes).size();
+ return (sizeOfUnion == 0) ? 1 : 1 - (double) sizeOfIntersection / sizeOfUnion;
+ }
+
+ @NotNull
+ private Set intersection(@NotNull Set set1, @NotNull Set set2) {
+ Set intersection = new HashSet<>(set1);
+ intersection.retainAll(set2);
+ return intersection;
+ }
+
+ @NotNull
+ private Set union(@NotNull Set set1, @NotNull Set set2) {
+ Set union = new HashSet<>(set1);
+ union.addAll(set2);
+ return union;
+ }
+
+ @NotNull
+ @Override
+ public List requiredMetrics() {
+ return Collections.emptyList();
+ }
+}
diff --git a/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/entity/OldEntity.java b/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/entity/OldEntity.java
index 4443438a..57c939f7 100644
--- a/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/entity/OldEntity.java
+++ b/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/entity/OldEntity.java
@@ -26,18 +26,15 @@
public abstract class OldEntity {
private static final VectorCalculator CLASS_ENTITY_CALCULATOR = new VectorCalculator()
.addMetricDependence(NumMethodsClassMetric.class)
- .addMetricDependence(NumAttributesAddedMetric.class)
- ;
+ .addMetricDependence(NumAttributesAddedMetric.class);
private static final VectorCalculator METHOD_ENTITY_CALCULATOR = new VectorCalculator()
.addConstValue(0)
- .addConstValue(0)
- ;
+ .addConstValue(0);
private static final VectorCalculator FIELD_ENTITY_CALCULATOR = new VectorCalculator()
.addConstValue(0)
- .addConstValue(0)
- ;
+ .addConstValue(0);
private static final int DIMENSION = CLASS_ENTITY_CALCULATOR.getDimension();
@@ -68,6 +65,7 @@ public PsiElement getPsiElement() {
protected OldEntity(OldEntity original) {
relevantProperties = original.relevantProperties.copy();
name = original.name;
+ element = original.element;
vector = Arrays.copyOf(original.vector, original.vector.length);
isMovable = original.isMovable;
}
@@ -164,4 +162,4 @@ public boolean isMovable() {
abstract public OldEntity copy();
abstract public boolean isField();
-}
+}
\ No newline at end of file
diff --git a/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/entity/RelevantProperties.java b/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/entity/RelevantProperties.java
index c8ba6c7e..3be7ffa7 100644
--- a/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/entity/RelevantProperties.java
+++ b/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/entity/RelevantProperties.java
@@ -1,8 +1,11 @@
package org.jetbrains.research.groups.ml_methods.algorithm.entity;
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiMethod;
+import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.HashMap;
@@ -18,7 +21,10 @@
* weight which corresponds to importance of this property.
*/
public class RelevantProperties {
-
+ @NotNull
+ private final Multiset bag = HashMultiset.create();
+ @NotNull
+ private final Map contextualVector;
private final Map methods = new HashMap<>();
private final Map classes = new HashMap<>();
private final Map fields = new HashMap<>();
@@ -26,6 +32,10 @@ public class RelevantProperties {
private final Integer DEFAULT_PROPERTY_WEIGHT = 1;
+ public RelevantProperties() {
+ contextualVector = new HashMap<>();
+ }
+
void removeMethod(String method) {
methods.remove(method);
}
@@ -176,10 +186,22 @@ public int sizeOfUnion(RelevantProperties other) {
public RelevantProperties copy() {
final RelevantProperties copy = new RelevantProperties();
+ copy.bag.addAll(bag);
+ copy.contextualVector.putAll(contextualVector);
copy.classes.putAll(classes);
copy.allMethods.putAll(allMethods);
copy.methods.putAll(methods);
copy.fields.putAll(fields);
return copy;
}
+
+ @NotNull
+ Multiset getBag() {
+ return bag;
+ }
+
+ @NotNull
+ public Map getContextualVector() {
+ return contextualVector;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/entity/RmmrEntitySearcher.java b/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/entity/RmmrEntitySearcher.java
new file mode 100644
index 00000000..d2ba8f92
--- /dev/null
+++ b/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/entity/RmmrEntitySearcher.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright 2018 Machine Learning Methods in Software Engineering Group of JetBrains Research
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jetbrains.research.groups.ml_methods.algorithm.entity;
+
+import com.google.common.collect.Multiset;
+import com.intellij.analysis.AnalysisScope;
+import com.intellij.openapi.progress.EmptyProgressIndicator;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.psi.*;
+import com.port.stemmer.Stemmer;
+import org.apache.log4j.Logger;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.research.groups.ml_methods.algorithm.properties.finder_strategy.RmmrStrategy;
+import org.jetbrains.research.groups.ml_methods.config.Logging;
+import org.jetbrains.research.groups.ml_methods.utils.IdentifierTokenizer;
+
+import java.util.*;
+
+import static com.google.common.math.DoubleMath.log2;
+
+/** Implementation of {@link OldEntity} searcher for RMMR algorithm */
+public class RmmrEntitySearcher {
+ private static final Logger LOGGER = Logging.getLogger(EntitySearcher.class);
+ private final Map classForName = new HashMap<>();
+ private final Map entities = new HashMap<>();
+ private final Map classEntities = new HashMap<>();
+ /** Terms are all words for contextual distance, for example: methodWithName gives as three terms: method, with, name */
+ private final Set terms = new HashSet<>();
+ /**
+ * Uniqueness property of term in whole document system (only classes are documents here)
+ * idf(term) = log_2(|D| / |{d \in D: t \in d}|)
+ */
+ private final Map idf = new HashMap<>();
+ private final List> documents = Arrays.asList(classEntities.values(), entities.values());
+ private final Stemmer stemmer = new Stemmer();
+ /** Scope where entities will be searched */
+ private final AnalysisScope scope;
+ /** Time when started search for entities */
+ private final long startTime = System.currentTimeMillis();
+ /** Strategy: which classes, methods and etc. to accept. For details see {@link RmmrStrategy} */
+ private final RmmrStrategy strategy = RmmrStrategy.getInstance();
+
+ {
+ strategy.setAcceptPrivateMethods(true);
+ strategy.setAcceptMethodParams(true);
+ strategy.setAcceptNewExpressions(true);
+ strategy.setAcceptMethodReferences(true);
+ strategy.setAcceptClassReferences(true);
+ strategy.setAcceptInnerClasses(true);
+ strategy.setApplyStemming(true);
+ strategy.setMinimalTermLength(1);
+ strategy.setCheckPsiVariableForBeingInScope(true);
+ }
+
+ /** UI progress indicator */
+ private final ProgressIndicator indicator;
+
+ /**
+ * Constructor which initializes indicator, startTime and saves given scope.
+ *
+ * @param scope where to search for entities.
+ */
+ private RmmrEntitySearcher(AnalysisScope scope) {
+ this.scope = scope;
+ if (ProgressManager.getInstance().hasProgressIndicator()) {
+ indicator = ProgressManager.getInstance().getProgressIndicator();
+ } else {
+ indicator = new EmptyProgressIndicator();
+ }
+ }
+
+ /**
+ * Finds and returns entities in given scope.
+ * @param scope where to search.
+ * @return search results described by {@link EntitySearchResult} object.
+ */
+ @NotNull
+ public static EntitySearchResult analyze(AnalysisScope scope) {
+ final RmmrEntitySearcher finder = new RmmrEntitySearcher(scope);
+ return finder.runCalculations();
+ }
+
+ /**
+ * Runs all calculations for searching.
+ * @return search results described by {@link EntitySearchResult} object.
+ */
+ @NotNull
+ private EntitySearchResult runCalculations() {
+ indicator.pushState();
+ indicator.setText("Searching entities");
+ indicator.setIndeterminate(true);
+ LOGGER.info("Indexing entities...");
+ scope.accept(new UnitsFinder());
+ indicator.setIndeterminate(false);
+ LOGGER.info("Calculating properties...");
+ indicator.setText("Calculating properties");
+ scope.accept(new PropertiesCalculator());
+ calculateContextualVectors();
+ indicator.popState();
+ return prepareResult();
+ }
+
+ private void calculateContextualVectors() {
+ calculateTf();
+ calculateIdf();
+ calculateTfIdf();
+ }
+
+ private void calculateTfIdf() {
+ for (Collection extends OldEntity> partOfDocuments : documents) {
+ for (OldEntity document : partOfDocuments) {
+ document.getRelevantProperties().getContextualVector().replaceAll((term, normalizedTf) -> normalizedTf * idf.get(term));
+ }
+ }
+ }
+
+ private void calculateIdf() {
+ long N = classEntities.size();
+ for (String term : terms) {
+ long tfInAllClasses = classEntities.values().stream().
+ filter(classEntity -> classEntity.getRelevantProperties().getBag().contains(term)).count();
+ idf.put(term, log2((double) N / tfInAllClasses));
+ }
+ }
+
+ private void calculateTf() {
+ for (Collection extends OldEntity> partOfDocuments : documents) {
+ for (OldEntity document : partOfDocuments) {
+ Multiset bag = document.getRelevantProperties().getBag();
+ for (Multiset.Entry term : bag.entrySet()) {
+ document.getRelevantProperties().getContextualVector().put(term.getElement(), 1 + log2(term.getCount()));
+ }
+ terms.addAll(bag.elementSet());
+ }
+ }
+ }
+
+ /**
+ * Creates {@link EntitySearchResult} instance based on found entities (sorts by classes, methods and etc.).
+ * @return search results described by {@link EntitySearchResult} object.
+ */
+ @NotNull
+ private EntitySearchResult prepareResult() {
+ LOGGER.info("Preparing results...");
+ final List classes = new ArrayList<>();
+ final List methods = new ArrayList<>();
+ for (MethodOldEntity methodEntity : entities.values()) {
+ indicator.checkCanceled();
+ methods.add(methodEntity);
+ }
+ for (ClassOldEntity classEntity : classEntities.values()) {
+ indicator.checkCanceled();
+ classes.add(classEntity);
+ }
+ LOGGER.info("Properties calculated");
+ LOGGER.info("Generated " + classes.size() + " class entities");
+ LOGGER.info("Generated " + methods.size() + " method entities");
+ LOGGER.info("Generated " + 0 + " field entities. Fields are not supported.");
+ return new EntitySearchResult(classes, methods, Collections.emptyList(), System.currentTimeMillis() - startTime);
+ }
+
+ /** Finds all units (classes, methods and etc.) in the scope based on {@link RmmrStrategy} that will be considered in searching process */
+ private class UnitsFinder extends JavaRecursiveElementVisitor {
+ @Override
+ public void visitFile(PsiFile file) {
+ indicator.checkCanceled();
+ if (!strategy.acceptFile(file)) {
+ return;
+ }
+ LOGGER.info("Indexing " + file.getName());
+ super.visitFile(file);
+ }
+
+ @Override
+ public void visitClass(PsiClass aClass) {
+ indicator.checkCanceled();
+ classForName.put(aClass.getQualifiedName(), aClass); // Classes for ConceptualSet.
+ if (!strategy.acceptClass(aClass)) {
+ return;
+ }
+ classEntities.put(aClass, new ClassOldEntity(aClass)); // Classes where method can be moved.
+ super.visitClass(aClass);
+ }
+
+ @Override
+ public void visitMethod(PsiMethod method) {
+ indicator.checkCanceled();
+ if (!strategy.acceptMethod(method)) {
+ return;
+ }
+ entities.put(method, new MethodOldEntity(method));
+ super.visitMethod(method);
+ }
+ }
+
+
+ /** Calculates conceptual sets and term bags for all methods and classes found by {@link UnitsFinder} */
+ // TODO: calculate properties for constructors? If yes, then we need to separate methods to check on refactoring (entities) and methods for calculating metric (to gather properties).
+ private class PropertiesCalculator extends JavaRecursiveElementVisitor {
+ private int propertiesCalculated = 0;
+ /** Stack of current classes (it has size more than 1 if we have nested classes), updates only the last bag */
+ // TODO: maybe update all bags on stack?
+ final private Deque currentClasses = new ArrayDeque<>();
+ /** Current method: if not null then we are parsing this method now and we need to update conceptual set and term bag of this method */
+ private MethodOldEntity currentMethod;
+
+ private void addIdentifierToBag(@Nullable OldEntity entity, String identifier) {
+ if (entity != null) {
+ List terms = IdentifierTokenizer.tokenize(identifier);
+ terms.removeIf(s -> s.length() < strategy.getMinimalTermLength());
+ if (strategy.isApplyStemming()) {
+ terms.replaceAll(s -> {
+ stemmer.add(s.toCharArray(), s.length());
+ stemmer.stem();
+ return stemmer.toString();
+ });
+ }
+ entity.getRelevantProperties().getBag().addAll(terms);
+ }
+ }
+
+ @Override
+ public void visitMethodReferenceExpression(PsiMethodReferenceExpression expression) {
+ indicator.checkCanceled();
+ if (strategy.isAcceptMethodReferences()) {
+ PsiElement expressionElement = expression.resolve();
+ if (expressionElement instanceof PsiMethod) {
+ processMethod((PsiMethod) expressionElement);
+ }
+ }
+ super.visitMethodReferenceExpression(expression);
+ }
+
+ private void processMethod(@NotNull PsiMethod calledMethod) {
+ final PsiClass usedClass = calledMethod.getContainingClass();
+ if (isClassInScope(usedClass)) {
+ addIdentifierToBag(currentClasses.peek(), calledMethod.getName());
+ addIdentifierToBag(currentMethod, calledMethod.getName());
+ /* Conceptual set part */
+ if (currentMethod != null) {
+ currentMethod.getRelevantProperties().addClass(usedClass);
+ }
+ }
+ }
+
+ @Override
+ public void visitVariable(PsiVariable variable) {
+ indicator.checkCanceled();
+ PsiClass variablesClass = null;
+ String variableName = variable.getName();
+ PsiType variableType = variable.getType();
+ if (variableType instanceof PsiClassType) {
+ variablesClass = ((PsiClassType) variableType).resolve();
+ }
+ // TODO: add support for arrays int[][][][][].
+ if (isClassInScope(variablesClass) && variablesClass.getName() != null) {
+ addIdentifierToBag(currentClasses.peek(), variablesClass.getName());
+ addIdentifierToBag(currentMethod, variablesClass.getName());
+ }
+ addIdentifierToBag(currentClasses.peek(), variableName);
+ addIdentifierToBag(currentMethod, variableName);
+ super.visitVariable(variable);
+ }
+
+ @Override
+ public void visitClass(PsiClass aClass) {
+ indicator.checkCanceled();
+ final ClassOldEntity classEntity = classEntities.get(aClass);
+ if (classEntity == null) {
+ super.visitClass(aClass);
+ return;
+ }
+ if (currentClasses.size() == 0 || strategy.isAcceptInnerClasses()) {
+ currentClasses.push(classEntity);
+ }
+ addIdentifierToBag(currentClasses.peek(), aClass.getName());
+ super.visitClass(aClass);
+ if (currentClasses.peek() == classEntity) {
+ currentClasses.pop();
+ }
+ }
+
+ @Override
+ public void visitMethod(PsiMethod method) {
+ indicator.checkCanceled();
+ addIdentifierToBag(currentClasses.peek(), method.getName());
+ final MethodOldEntity methodEntity = entities.get(method);
+ if (methodEntity == null) {
+ super.visitMethod(method);
+ return;
+ }
+ if (currentMethod == null) {
+ currentMethod = methodEntity;
+ }
+ addIdentifierToBag(currentMethod, method.getName());
+ if (strategy.isAcceptMethodParams()) {
+ for (PsiParameter attribute : method.getParameterList().getParameters()) {
+ PsiType attributeType = attribute.getType();
+ if (attributeType instanceof PsiClassType) {
+ PsiClass aClass = ((PsiClassType) attributeType).resolve();
+ if (isClassInScope(aClass)) {
+ currentMethod.getRelevantProperties().addClass(aClass);
+ }
+ }
+ }
+ }
+
+ super.visitMethod(method);
+ if (currentMethod == methodEntity) {
+ currentMethod = null;
+ }
+ reportPropertiesCalculated();
+ }
+
+ @Override
+ public void visitReferenceExpression(PsiReferenceExpression expression) {
+ indicator.checkCanceled();
+ final PsiElement expressionElement = expression.resolve();
+ if (expressionElement instanceof PsiVariable) {
+ boolean isInScope = !strategy.getCheckPsiVariableForBeingInScope() ||
+ (!(expressionElement instanceof PsiField) ||
+ isClassInScope(((PsiField) expressionElement).getContainingClass()));
+ if (isInScope) {
+ addIdentifierToBag(currentClasses.peek(), ((PsiVariable) expressionElement).getName());
+ addIdentifierToBag(currentMethod, ((PsiVariable) expressionElement).getName());
+ }
+ /* Conceptual Set part */
+ if (expressionElement instanceof PsiField) {
+ PsiField attribute = (PsiField) expressionElement;
+ final PsiClass attributeClass = attribute.getContainingClass();
+ if (currentMethod != null && isClassInScope(attributeClass)) {
+ currentMethod.getRelevantProperties().addClass(attributeClass);
+ }
+ }
+ }
+ if (strategy.isAcceptClassReferences() && expressionElement instanceof PsiClass) {
+ PsiClass aClass = (PsiClass) expressionElement;
+ if (isClassInScope(aClass)) {
+ addIdentifierToBag(currentClasses.peek(), aClass.getName());
+ addIdentifierToBag(currentMethod, aClass.getName());
+ }
+ }
+ super.visitReferenceExpression(expression);
+ }
+
+ @Override
+ public void visitNewExpression(PsiNewExpression expression) {
+ if (strategy.isAcceptNewExpressions()) {
+ indicator.checkCanceled();
+ PsiType type = expression.getType();
+ PsiClass usedClass = type instanceof PsiClassType ? ((PsiClassType) type).resolve() : null;
+ if (currentMethod != null && isClassInScope(usedClass)) {
+ currentMethod.getRelevantProperties().addClass(usedClass);
+ }
+ }
+ super.visitNewExpression(expression);
+ }
+
+ @Override
+ public void visitMethodCallExpression(PsiMethodCallExpression expression) {
+ final PsiMethod called = expression.resolveMethod();
+ if (called != null) {
+ processMethod(called);
+ }
+ super.visitMethodCallExpression(expression);
+ }
+
+ private void reportPropertiesCalculated() {
+ propertiesCalculated++;
+ if (indicator != null) {
+ indicator.setFraction((double) propertiesCalculated / entities.size());
+ }
+ }
+ }
+
+ @Contract("null -> false")
+ private boolean isClassInScope(final @Nullable PsiClass aClass) {
+ return aClass != null && classForName.containsKey(aClass.getQualifiedName());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/properties/finder_strategy/RmmrStrategy.java b/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/properties/finder_strategy/RmmrStrategy.java
new file mode 100644
index 00000000..fc057fb1
--- /dev/null
+++ b/src/main/java/org/jetbrains/research/groups/ml_methods/algorithm/properties/finder_strategy/RmmrStrategy.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2018 Machine Learning Methods in Software Engineering Group of JetBrains Research
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jetbrains.research.groups.ml_methods.algorithm.properties.finder_strategy;
+
+import com.intellij.psi.*;
+import com.sixrr.metrics.utils.ClassUtils;
+import com.sixrr.metrics.utils.MethodUtils;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Describes strategy for searching entities.
+ * For example: we do not accept enum classes because method couldn't (very rarely) be moved there.
+ * This implementation is singleton object.
+ */
+public class RmmrStrategy implements FinderStrategy {
+ private static RmmrStrategy INSTANCE = new RmmrStrategy();
+ private boolean acceptPrivateMethods;
+ private boolean acceptMethodParams;
+ /**
+ * Check for expressions like this: instanceOfClassNotInScope.publicFieldWithNoInformationForContext.
+ * This situation may occur when field is public or protected or package private but we do not consider its class
+ * (because it can be external class in jar or something like that).
+ */
+ private boolean checkPsiVariableForBeingInScope;
+ private boolean acceptNewExpressions;
+ private boolean acceptInnerClasses;
+ private boolean applyStemming;
+ private int minimalTermLength;
+ private boolean acceptMethodReferences;
+ private boolean acceptClassReferences;
+
+ /**
+ * Get instance of singleton object.
+ *
+ * @return instance of this class.
+ */
+ @NotNull
+ public static RmmrStrategy getInstance() {
+ return INSTANCE;
+ }
+
+ private RmmrStrategy() {
+ }
+
+ @Override
+ public boolean acceptClass(@NotNull PsiClass aClass) {
+ // TODO: Accept interfaces or not?
+ // TODO: Accept inner and nested classes or not?
+ return !(ClassUtils.isAnonymous(aClass) || aClass.getQualifiedName() == null
+ || aClass.isEnum() || aClass.isInterface());
+ }
+
+ @Override
+ public boolean acceptMethod(@NotNull PsiMethod method) {
+ // TODO: accept in interfaces?
+ if (method.isConstructor() || MethodUtils.isAbstract(method)) {
+ return false;
+ }
+ if (!acceptPrivateMethods && method.getModifierList().hasModifierProperty(PsiModifier.PRIVATE)) {
+ return false;
+ }
+ final PsiClass containingClass = method.getContainingClass();
+ return !(containingClass == null || containingClass.isInterface() || !acceptClass(containingClass));
+ }
+
+ @Override
+ public boolean acceptField(@NotNull PsiField field) {
+ return false;
+ }
+
+ @Override
+ public boolean isRelation(@NotNull PsiElement element) {
+ return true;
+ }
+
+ @Override
+ public boolean processSupers() {
+ return false;
+ }
+
+ @Override
+ public int getWeight(PsiMethod from, PsiClass to) {
+ return DEFAULT_WEIGHT;
+ }
+
+ @Override
+ public int getWeight(PsiMethod from, PsiField to) {
+ return DEFAULT_WEIGHT;
+ }
+
+ @Override
+ public int getWeight(PsiMethod from, PsiMethod to) {
+ return DEFAULT_WEIGHT;
+ }
+
+ @Override
+ public int getWeight(PsiClass from, PsiField to) {
+ return DEFAULT_WEIGHT;
+ }
+
+ @Override
+ public int getWeight(PsiClass from, PsiMethod to) {
+ return DEFAULT_WEIGHT;
+ }
+
+ @Override
+ public int getWeight(PsiClass from, PsiClass to) {
+ return DEFAULT_WEIGHT;
+ }
+
+ @Override
+ public int getWeight(PsiField from, PsiField to) {
+ return 0;
+ }
+
+ @Override
+ public int getWeight(PsiField from, PsiMethod to) {
+ return 0;
+ }
+
+ @Override
+ public int getWeight(PsiField from, PsiClass to) {
+ return 0;
+ }
+
+ public void setAcceptPrivateMethods(boolean acceptPrivateMethods) {
+ this.acceptPrivateMethods = acceptPrivateMethods;
+ }
+
+ public void setCheckPsiVariableForBeingInScope(boolean checkPsiVariableForBeingInScope) {
+ this.checkPsiVariableForBeingInScope = checkPsiVariableForBeingInScope;
+ }
+
+ public boolean getCheckPsiVariableForBeingInScope() {
+ return checkPsiVariableForBeingInScope;
+ }
+
+ public boolean isAcceptMethodParams() {
+ return acceptMethodParams;
+ }
+
+ public void setAcceptMethodParams(boolean acceptMethodParams) {
+ this.acceptMethodParams = acceptMethodParams;
+ }
+
+ public boolean isAcceptNewExpressions() {
+ return acceptNewExpressions;
+ }
+
+ public void setAcceptNewExpressions(boolean acceptNewExpressions) {
+ this.acceptNewExpressions = acceptNewExpressions;
+ }
+
+ public void setAcceptInnerClasses(boolean acceptInnerClasses) {
+ this.acceptInnerClasses = acceptInnerClasses;
+ }
+
+ public boolean isAcceptInnerClasses() {
+ return acceptInnerClasses;
+ }
+
+ public void setApplyStemming(boolean applyStemming) {
+ this.applyStemming = applyStemming;
+ }
+
+ public boolean isApplyStemming() {
+ return applyStemming;
+ }
+
+ public int getMinimalTermLength() {
+ return minimalTermLength;
+ }
+
+ public void setMinimalTermLength(int minimalTermLength) {
+ this.minimalTermLength = minimalTermLength;
+ }
+
+ public boolean isAcceptMethodReferences() {
+ return acceptMethodReferences;
+ }
+
+ public void setAcceptMethodReferences(boolean acceptMethodReferences) {
+ this.acceptMethodReferences = acceptMethodReferences;
+ }
+
+ public boolean isAcceptClassReferences() {
+ return acceptClassReferences;
+ }
+
+ public void setAcceptClassReferences(boolean acceptClassReferences) {
+ this.acceptClassReferences = acceptClassReferences;
+ }
+}
diff --git a/src/main/java/org/jetbrains/research/groups/ml_methods/refactoring/RefactoringExecutionContext.java b/src/main/java/org/jetbrains/research/groups/ml_methods/refactoring/RefactoringExecutionContext.java
index 409e3091..03cd989f 100644
--- a/src/main/java/org/jetbrains/research/groups/ml_methods/refactoring/RefactoringExecutionContext.java
+++ b/src/main/java/org/jetbrains/research/groups/ml_methods/refactoring/RefactoringExecutionContext.java
@@ -14,14 +14,14 @@
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import org.jetbrains.research.groups.ml_methods.algorithm.*;
+import org.jetbrains.research.groups.ml_methods.algorithm.entity.RmmrEntitySearcher;
import org.jetbrains.research.groups.ml_methods.algorithm.attributes.AttributesStorage;
import org.jetbrains.research.groups.ml_methods.algorithm.attributes.NoRequestedMetricException;
import org.jetbrains.research.groups.ml_methods.algorithm.entity.EntitiesStorage;
-import org.jetbrains.research.groups.ml_methods.algorithm.*;
import org.jetbrains.research.groups.ml_methods.algorithm.entity.EntitySearchResult;
import org.jetbrains.research.groups.ml_methods.algorithm.entity.EntitySearcher;
import org.jetbrains.research.groups.ml_methods.config.Logging;
-
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -39,7 +39,8 @@ public class RefactoringExecutionContext {
new AKMeans(),
new CCDA(),
new HAC(),
- new MRI()
+ new MRI(),
+ new RMMR()
);
@NotNull
@@ -118,10 +119,17 @@ private void execute(ProgressIndicator indicator) {
metricsRun.setProfileName(profile.getName());
metricsRun.setContext(scope);
metricsRun.setTimestamp(new TimeStamp());
- entitySearchResult = ApplicationManager.getApplication()
- .runReadAction((Computable) () -> EntitySearcher.analyze(scope, metricsRun));
- entitiesStorage = new EntitiesStorage(entitySearchResult);
for (Algorithm algorithm : requestedAlgorithms) {
+ switch (algorithm.getDescriptionString()) {
+ case RMMR.NAME:
+ entitySearchResult = ApplicationManager.getApplication()
+ .runReadAction((Computable) () -> RmmrEntitySearcher.analyze(scope));
+ break;
+ default:
+ entitySearchResult = ApplicationManager.getApplication()
+ .runReadAction((Computable) () -> EntitySearcher.analyze(scope, metricsRun));
+ }
+ entitiesStorage = new EntitiesStorage(entitySearchResult);
calculate(algorithm);
}
indicator.setText("Finish refactorings search...");
diff --git a/src/main/java/org/jetbrains/research/groups/ml_methods/utils/IdentifierTokenizer.java b/src/main/java/org/jetbrains/research/groups/ml_methods/utils/IdentifierTokenizer.java
new file mode 100644
index 00000000..1adb35b9
--- /dev/null
+++ b/src/main/java/org/jetbrains/research/groups/ml_methods/utils/IdentifierTokenizer.java
@@ -0,0 +1,27 @@
+package org.jetbrains.research.groups.ml_methods.utils;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class IdentifierTokenizer {
+ @NotNull
+ public static List tokenize(@NotNull String identifier) {
+ if (identifier.startsWith("_")) {
+ identifier = identifier.substring(1);
+ }
+ List tokenizedString = new LinkedList<>();
+ String regexpSplit;
+ if (identifier.toUpperCase().equals(identifier)) {
+ regexpSplit = "_";
+ }
+ else {
+ regexpSplit = "(?=\\p{Lu})";
+ }
+ tokenizedString.addAll(Arrays.asList(identifier.split(regexpSplit)));
+ return tokenizedString.stream().map(String::toLowerCase).collect(Collectors.toList());
+ }
+}
diff --git a/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/AlgorithmAbstractTest.java b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/AlgorithmAbstractTest.java
index f211aacd..87375a6e 100644
--- a/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/AlgorithmAbstractTest.java
+++ b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/AlgorithmAbstractTest.java
@@ -16,8 +16,6 @@
@SuppressWarnings("WeakerAccess")
public abstract class AlgorithmAbstractTest extends LightCodeInsightFixtureTestCase {
- protected final TestCasesCheckers testCasesChecker = new TestCasesCheckers(getAlgorithm().getDescriptionString());
-
@Override
protected String getTestDataPath() {
return "src/test/resources/testCases/" + getTestName(true);
diff --git a/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/AriTest.java b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/AriTest.java
index 1625bb2b..022a9022 100644
--- a/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/AriTest.java
+++ b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/AriTest.java
@@ -2,8 +2,8 @@
public class AriTest extends AlgorithmAbstractTest {
private static final Algorithm algorithm = new ARI();
-
private static final String algorithmName = algorithm.getDescriptionString();
+ private static final TestCasesCheckers testCasesChecker = new TestCasesCheckers(algorithmName, true);
public void testMoveMethod() {
executeTest(testCasesChecker::checkMoveMethod, "ClassA.java", "ClassB.java");
diff --git a/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/CcdaTest.java b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/CcdaTest.java
index 3eddb483..ec35402a 100644
--- a/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/CcdaTest.java
+++ b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/CcdaTest.java
@@ -2,10 +2,8 @@
public class CcdaTest extends AlgorithmAbstractTest {
private static final Algorithm algorithm = new CCDA();
-
private static final String algorithmName = algorithm.getDescriptionString();
-
- private static final TestCasesCheckers testCasesChecker = new TestCasesCheckers(algorithmName);
+ private static final TestCasesCheckers testCasesChecker = new TestCasesCheckers(algorithmName, true);
public void testMoveMethod() {
executeTest(testCasesChecker::checkMoveMethod, "ClassA.java", "ClassB.java");
diff --git a/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/HacTest.java b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/HacTest.java
index 3c3928a9..06a065ee 100644
--- a/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/HacTest.java
+++ b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/HacTest.java
@@ -2,9 +2,8 @@
public class HacTest extends AlgorithmAbstractTest {
private static final Algorithm algorithm = new HAC();
-
private static final String algorithmName = algorithm.getDescriptionString();
- private static final TestCasesCheckers testCasesChecker = new TestCasesCheckers(algorithmName);
+ private static final TestCasesCheckers testCasesChecker = new TestCasesCheckers(algorithmName, true);
public void testMoveMethod() {
executeTest(testCasesChecker::checkMoveMethod, "ClassA.java", "ClassB.java");
diff --git a/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/RmmrDistancesTest.java b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/RmmrDistancesTest.java
new file mode 100644
index 00000000..c5f52a3e
--- /dev/null
+++ b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/RmmrDistancesTest.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2018 Machine Learning Methods in Software Engineering Group of JetBrains Research
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jetbrains.research.groups.ml_methods.algorithm;
+
+import com.intellij.analysis.AnalysisScope;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.research.groups.ml_methods.algorithm.entity.ClassOldEntity;
+import org.jetbrains.research.groups.ml_methods.algorithm.entity.EntitySearchResult;
+import org.jetbrains.research.groups.ml_methods.algorithm.entity.MethodOldEntity;
+import org.jetbrains.research.groups.ml_methods.algorithm.entity.RmmrEntitySearcher;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.stream.Collectors;
+
+// TODO: tests tightly depend on RMMR configs, but there is a lot of possible configurations. Rewrite or leave only one config.
+public class RmmrDistancesTest extends LightCodeInsightFixtureTestCase {
+ private EntitySearchResult searchResult;
+ private RMMR algorithm = new RMMR();
+ private Method getDistanceWithMethod;
+ private Method getDistanceWithClass;
+
+ @Override
+ protected String getTestDataPath() {
+ return "src/test/resources/testCases/" + getPackage();
+ }
+
+ @NotNull
+ @Contract(pure = true)
+ private String getPackage() {
+ return "movieRentalStoreWithFeatureEnvy";
+ }
+
+ private void init() throws NoSuchMethodException {
+ final VirtualFile customer = myFixture.copyFileToProject("Customer.java");
+ final VirtualFile movie = myFixture.copyFileToProject("Movie.java");
+ final VirtualFile rental = myFixture.copyFileToProject("Rental.java");
+
+ AnalysisScope analysisScope = new AnalysisScope(myFixture.getProject(), Arrays.asList(customer, movie, rental));
+ searchResult = RmmrEntitySearcher.analyze(analysisScope);
+ getDistanceWithMethod = RMMR.class.getDeclaredMethod("getConceptualDistance", MethodOldEntity.class, MethodOldEntity.class);
+ getDistanceWithMethod.setAccessible(true);
+ getDistanceWithClass = RMMR.class.getDeclaredMethod("getConceptualDistance",
+ MethodOldEntity.class, ClassOldEntity.class);
+ getDistanceWithClass.setAccessible(true);
+ }
+
+ private void setUpMethodsByClass() throws NoSuchFieldException, IllegalAccessException {
+ Field methodsByClass = RMMR.class.getDeclaredField("methodsByClass");
+ methodsByClass.setAccessible(true);
+ Map> methodsByClassToSet = new HashMap<>();
+ searchResult.getMethods().forEach(methodEntity -> {
+ List methodClassEntity = searchResult.getClasses().stream()
+ .filter(classEntity -> methodEntity.getClassName().equals(classEntity.getName()))
+ .collect(Collectors.toList());
+ methodsByClassToSet.computeIfAbsent(methodClassEntity.get(0), anyKey -> new HashSet<>()).add(methodEntity);
+ });
+ methodsByClass.set(algorithm, methodsByClassToSet);
+ }
+
+ public void igonred_testDistanceBetweenMethods() throws Exception {
+ init();
+ checkGetMovieBetweenMethods();
+ checkAddRentalBetweenMethods();
+ checkGetDaysRentedBetweenMethods();
+ checkSetPriceCodeBetweenMethods();
+ }
+
+ public void testDistanceBetweenMethodAndClass() throws Exception {
+ init();
+ setUpMethodsByClass();
+ checkGetMovieWithClasses();
+ checkAddRentalWithClasses();
+ checkGetDaysRentedWithClasses();
+ checkSetPriceCodeWithClasses();
+ }
+
+ private void checkSetPriceCodeWithClasses() throws InvocationTargetException, IllegalAccessException {
+ String methodName = "Movie.setPriceCode(int)";
+
+ double expected = 0;
+ expected += runGetDistanceWithMethod(methodName, "Customer.getMovie(Movie)");
+ expected += runGetDistanceWithMethod(methodName, "Customer.addRental(Rental)");
+ expected += runGetDistanceWithMethod(methodName, "Customer.getName()");
+ expected /= 3;
+ checkGetDistanceWithClass(methodName, "Customer", expected);
+
+ expected = 0;
+ expected += runGetDistanceWithMethod(methodName, "Movie.getPriceCode()");
+ expected += runGetDistanceWithMethod(methodName, "Movie.getTitle()");
+ expected /= 2;
+ checkGetDistanceWithClass(methodName, "Movie", expected);
+
+ expected = 0;
+ expected += runGetDistanceWithMethod(methodName, "Rental.getDaysRented()");
+ expected /= 1;
+ checkGetDistanceWithClass(methodName, "Rental", expected);
+ }
+
+ private void checkGetDaysRentedWithClasses() throws InvocationTargetException, IllegalAccessException {
+ String methodName = "Rental.getDaysRented()";
+
+ double expected = 0;
+ expected += runGetDistanceWithMethod(methodName, "Customer.getMovie(Movie)");
+ expected += runGetDistanceWithMethod(methodName, "Customer.addRental(Rental)");
+ expected += runGetDistanceWithMethod(methodName, "Customer.getName()");
+ expected /= 3;
+ checkGetDistanceWithClass(methodName, "Customer", expected);
+
+ expected = 0;
+ expected += runGetDistanceWithMethod(methodName, "Movie.getPriceCode()");
+ expected += runGetDistanceWithMethod(methodName, "Movie.setPriceCode(int)");
+ expected += runGetDistanceWithMethod(methodName, "Movie.getTitle()");
+ expected /= 3;
+ checkGetDistanceWithClass(methodName, "Movie", expected);
+
+ expected = 1;
+ checkGetDistanceWithClass(methodName, "Rental", expected);
+ }
+
+ private void checkAddRentalWithClasses() throws InvocationTargetException, IllegalAccessException {
+ String methodName = "Customer.addRental(Rental)";
+
+ double expected = 0;
+ expected += runGetDistanceWithMethod(methodName, "Customer.getMovie(Movie)");
+ expected += runGetDistanceWithMethod(methodName, "Customer.getName()");
+ expected /= 2;
+ checkGetDistanceWithClass(methodName, "Customer", expected);
+
+ expected = 0;
+ expected += runGetDistanceWithMethod(methodName, "Movie.getPriceCode()");
+ expected += runGetDistanceWithMethod(methodName, "Movie.setPriceCode(int)");
+ expected += runGetDistanceWithMethod(methodName, "Movie.getTitle()");
+ expected /= 3;
+ checkGetDistanceWithClass(methodName, "Movie", expected);
+
+ expected = 0;
+ expected += runGetDistanceWithMethod(methodName, "Rental.getDaysRented()");
+ expected /= 1;
+ checkGetDistanceWithClass(methodName, "Rental", expected);
+ }
+
+ private void checkGetMovieWithClasses() throws InvocationTargetException, IllegalAccessException {
+ String methodName = "Customer.getMovie(Movie)";
+
+ double expected = 0;
+ expected += runGetDistanceWithMethod(methodName, "Customer.addRental(Rental)");
+ expected += runGetDistanceWithMethod(methodName, "Customer.getName()");
+ expected /= 2;
+ checkGetDistanceWithClass(methodName, "Customer", expected);
+
+ expected = 0;
+ expected += runGetDistanceWithMethod(methodName, "Movie.getPriceCode()");
+ expected += runGetDistanceWithMethod(methodName, "Movie.setPriceCode(int)");
+ expected += runGetDistanceWithMethod(methodName, "Movie.getTitle()");
+ expected /= 3;
+ checkGetDistanceWithClass(methodName, "Movie", expected);
+
+ expected = 0;
+ expected += runGetDistanceWithMethod(methodName, "Rental.getDaysRented()");
+ expected /= 1;
+ checkGetDistanceWithClass(methodName, "Rental", expected);
+ }
+
+ private void checkSetPriceCodeBetweenMethods() throws InvocationTargetException, IllegalAccessException {
+ String methodName = "Movie.setPriceCode(int)";
+
+ checkGetDistanceWithMethod(methodName, "Customer.getMovie(Movie)", 1 - 1 / 2.0);
+ checkGetDistanceWithMethod(methodName, "Customer.addRental(Rental)", 1 - 0 / 2.0);
+ checkGetDistanceWithMethod(methodName, "Customer.getName()", 1 - 0 / 2.0);
+
+ checkGetDistanceWithMethod(methodName, "Movie.getPriceCode()", 1 - 1 / 1.0);
+ checkGetDistanceWithMethod(methodName, "Movie.setPriceCode(int)", 1 - 1 / 1.0);
+ checkGetDistanceWithMethod(methodName, "Movie.getTitle()", 1 - 1 / 1.0);
+
+ checkGetDistanceWithMethod(methodName, "Rental.getDaysRented()", 1 - 0 / 2.0);
+ }
+
+ private void checkGetDaysRentedBetweenMethods() throws InvocationTargetException, IllegalAccessException {
+ String methodName = "Rental.getDaysRented()";
+
+ checkGetDistanceWithMethod(methodName, "Customer.getMovie(Movie)", 1 - 1 / 2.0);
+ checkGetDistanceWithMethod(methodName, "Customer.addRental(Rental)", 1 - 0 / 2.0);
+ checkGetDistanceWithMethod(methodName, "Customer.getName()", 1 - 0 / 2.0);
+
+ checkGetDistanceWithMethod(methodName, "Movie.getPriceCode()", 1 - 0 / 2.0);
+ checkGetDistanceWithMethod(methodName, "Movie.setPriceCode(int)", 1 - 0 / 2.0);
+ checkGetDistanceWithMethod(methodName, "Movie.getTitle()", 1 - 0 / 2.0);
+
+ checkGetDistanceWithMethod(methodName, "Rental.getDaysRented()", 1 - 1 / 1.0);
+ }
+
+ private void checkAddRentalBetweenMethods() throws InvocationTargetException, IllegalAccessException {
+ String methodName = "Customer.addRental(Rental)";
+
+ checkGetDistanceWithMethod(methodName, "Customer.getMovie(Movie)", 1 - 0 / 3.0);
+ checkGetDistanceWithMethod(methodName, "Customer.addRental(Rental)", 1 - 1 / 1.0);
+ checkGetDistanceWithMethod(methodName, "Customer.getName()", 1 - 1 / 1.0);
+
+ checkGetDistanceWithMethod(methodName, "Movie.getPriceCode()", 1 - 0 / 2.0);
+ checkGetDistanceWithMethod(methodName, "Movie.setPriceCode(int)", 1 - 0 / 2.0);
+ checkGetDistanceWithMethod(methodName, "Movie.getTitle()", 1 - 0 / 2.0);
+
+ checkGetDistanceWithMethod(methodName, "Rental.getDaysRented()", 1 - 0 / 2.0);
+ }
+
+ private void checkGetMovieBetweenMethods() throws InvocationTargetException, IllegalAccessException {
+ String methodName = "Customer.getMovie(Movie)";
+
+ checkGetDistanceWithMethod(methodName, "Customer.getMovie(Movie)", 1 - 2 / 2.0);
+ checkGetDistanceWithMethod(methodName, "Customer.addRental(Rental)", 1 - 0 / 3.0);
+ checkGetDistanceWithMethod(methodName, "Customer.getName()", 1 - 0 / 3.0);
+
+ checkGetDistanceWithMethod(methodName, "Movie.getPriceCode()", 1 - 1 / 2.0);
+ checkGetDistanceWithMethod(methodName, "Movie.setPriceCode(int)", 1 - 1 / 2.0);
+ checkGetDistanceWithMethod(methodName, "Movie.getTitle()", 1 - 1 / 2.0);
+
+ checkGetDistanceWithMethod(methodName, "Rental.getDaysRented()", 1 - 1 / 2.0);
+ }
+
+ private void checkGetDistanceWithMethod(String methodName1, String methodName2, double expected) throws InvocationTargetException, IllegalAccessException {
+ assertEquals(expected, runGetDistanceWithMethod(methodName1, methodName2));
+ }
+
+ private Double runGetDistanceWithMethod(String methodName1, String methodName2) throws InvocationTargetException, IllegalAccessException {
+ MethodOldEntity methodEntity1 = searchResult.getMethods().stream().
+ filter(methodEntity -> methodEntity.getName().equals(getPackage() + "." + methodName1)).
+ findAny().orElseThrow(NoSuchElementException::new);
+ MethodOldEntity methodEntity2 = searchResult.getMethods().stream().
+ filter(methodEntity -> methodEntity.getName().equals(getPackage() + "." + methodName2)).
+ findAny().orElseThrow(NoSuchElementException::new);
+ return (Double) getDistanceWithMethod.invoke(algorithm, methodEntity1, methodEntity2);
+ }
+
+ private void checkGetDistanceWithClass(String methodName, String className, double expected) throws InvocationTargetException, IllegalAccessException {
+ MethodOldEntity methodEntity = searchResult.getMethods().stream().
+ filter(methodEntity2 -> methodEntity2.getName().equals(getPackage() + "." + methodName)).
+ findAny().orElseThrow(NoSuchElementException::new);
+ ClassOldEntity classEntity = searchResult.getClasses().stream().
+ filter(classEntity2 -> classEntity2.getName().equals(getPackage() + "." + className)).
+ findAny().orElseThrow(NoSuchElementException::new);
+ assertEquals(expected, getDistanceWithClass.invoke(algorithm, methodEntity, classEntity));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/RmmrTest.java b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/RmmrTest.java
new file mode 100644
index 00000000..7d2b8eaa
--- /dev/null
+++ b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/RmmrTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2018 Machine Learning Methods in Software Engineering Group of JetBrains Research
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jetbrains.research.groups.ml_methods.algorithm;
+
+public class RmmrTest extends AlgorithmAbstractTest {
+ private static final Algorithm algorithm = new RMMR();
+ private static final String algorithmName = algorithm.getDescriptionString();
+ private static final TestCasesCheckers testCasesChecker = new TestCasesCheckers(algorithmName, false);
+
+ public void testMoveMethod() {
+ executeTest(testCasesChecker::checkMoveMethod, "ClassA.java", "ClassB.java");
+ }
+
+ // TODO: Not currently supported
+ /*
+ Failure explanation: In terms of contextual similarity methodB1 has highest score with its own class,
+ and lower with Nested class. methodB1 has no body, so it's conceptual set is empty, that is why conceptual similarity
+ is very low, but if we add its own class (ClassB) to conceptual set of methodB1, then methodB1 -> Nested refactoring
+ will be suggested. But it will fail a lot of tests that pass now (only 6 will pass), so it is bad decision to
+ include its own class to method's conceptual set.
+ */
+ public void failing_testCallFromNested() {
+ executeTest(testCasesChecker::checkCallFromNested, "ClassA.java", "ClassB.java");
+ }
+
+ // TODO: Not currently supported
+ /*
+ Failure explanation: because we include statistic of processing method to class statistic it reflects
+ on dot product of method's and class's vectors. In this test case similarity is very high because of that
+ so algorithm doesn't find any refactorings. Dependency sets intersection is always empty so it doesn't affect the results.
+ */
+ public void failing_testCircularDependency() {
+ executeTest(testCasesChecker::checkCircularDependency, "ClassA.java", "ClassB.java", "ClassC.java");
+ }
+
+ // TODO: Not currently supported
+ /*
+ Failure explanation: interesting case because all methods with all classes have max distances - 1.
+ Contextual distance is max because all words appear everywhere, conceptual because intersection is empty.\
+ Consider: if we have big distance with source than maybe suggest a refactoring? (big doubts)
+ */
+ public void failing_testCrossReferencesMethods() {
+ executeTest(testCasesChecker::checkCrossReferencesMethods, "ClassA.java", "ClassB.java");
+ }
+
+ public void testDontMoveAbstract() {
+ executeTest(testCasesChecker::checkDontMoveAbstract, "ClassA.java", "ClassB.java");
+ }
+
+ public void testDontMoveConstructor() {
+ executeTest(testCasesChecker::checkDontMoveConstructor, "ClassA.java", "ClassB.java");
+ }
+
+ public void testDontMoveOverridden() {
+ executeTest(testCasesChecker::checkDontMoveOverridden, "ClassA.java", "ClassB.java");
+ }
+
+ // TODO: Not currently supported
+ public void failing_testMoveField() {
+ executeTest(testCasesChecker::checkMoveField, "ClassA.java", "ClassB.java");
+ }
+
+ // TODO: Not currently supported
+ /*
+ Failure explanation: methodB1 has conceptual distance 0 with ClassA, but contextual similarity distance is very high.
+ Meanwhile total distance with ClassB of methods methodB1 and methodB2 is less than 0.2 which is the least.
+ Again there are a lot of 0 in vectors because of appearance in both classes. That problem must disappear on big projects.
+ */
+ public void failing_testMoveTogether() {
+ executeTest(testCasesChecker::checkMoveTogether, "ClassA.java", "ClassB.java");
+ }
+
+ // TODO: Not currently supported
+ /*
+ Failure explanation: for methodA1 all distances are 1 because dependency set is empty,
+ and "a1" and "method" appear in both classes so in vector they are 0 coordinates.
+ Consider: problem is because it is two classes test case, otherwise we could count that a1
+ appears much more often in ClassB than in ClassA and context distance would be smaller.
+ */
+ public void failing_testPriority() {
+ executeTest(testCasesChecker::checkPriority, "ClassA.java", "ClassB.java");
+ }
+
+ public void testRecursiveMethod() {
+ executeTest(testCasesChecker::checkRecursiveMethod, "ClassA.java", "ClassB.java");
+ }
+
+ // TODO: Not currently supported
+ /*
+ Failure explanation: refactoring can be found if add to methods in ClassA some references to its own class to get
+ ClassA in conceptual set of this methods. Without that conceptual distance is always 1 and contextual distance
+ plays a big role and it is the lowest with source classes.
+ Consider: adding field attribute = "result" to ClassA solves the problem.
+ */
+ public void failing_testReferencesOnly() {
+ executeTest(testCasesChecker::checkReferencesOnly, "ClassA.java", "ClassB.java");
+ }
+
+ // TODO: Not currently supported
+ /*
+ Failure explanation: RMMR doesn't consider global dependency and structure, so it is prospective that this test fails.
+ Methods "methodToMove" from ClassB and ClassC have big distance with ClassA. Mostly because of contextual distance but
+ dependency distance is high too (even weights 0.7 and 0.3 doesn't solve a problem). With other two classes, for example ClassB.methodToMove,
+ has almost equal distances (about 0.5), but with it's own class distance is 0.5 because of contextual similarity,
+ and with other class because of conceptual similarity.
+ */
+ public void failing_testTriangularDependence() {
+ executeTest(testCasesChecker::checkTriangularDependence, "ClassA.java", "ClassB.java", "ClassC.java");
+ }
+
+ public void testMobilePhoneNoFeatureEnvy() {
+ executeTest(testCasesChecker::checkMobilePhoneNoFeatureEnvy, "Customer.java", "Phone.java");
+ }
+
+ // TODO: Not currently supported
+ /*
+ Failure explanation: almost all words appear in both classes that is why idf is 0.
+ As a result vector is something like that: 3, 0, 0, ..., 0.
+ And there is no intersection with not nulls so context similarity is 0.
+ getMobilePhone method has big distance (almost 1) with its class and big dissimilarity with Phone class.
+ But own class (Customer) wins...
+ */
+ public void failing_testMobilePhoneWithFeatureEnvy() {
+ executeTest(testCasesChecker::checkMobilePhoneWithFeatureEnvy, "Customer.java", "Phone.java");
+ }
+
+ public void testMovieRentalStoreNoFeatureEnvy() {
+ executeTest(testCasesChecker::checkMovieRentalStoreNoFeatureEnvy, "Customer.java", "Movie.java", "Rental.java");
+ }
+
+ public void testMovieRentalStoreWithFeatureEnvy() {
+ executeTest(testCasesChecker::checkMovieRentalStoreWithFeatureEnvy, "Customer.java", "Movie.java", "Rental.java");
+ }
+
+ // TODO: Not currently supported
+ /*
+ Failure explanation: the same problem as in references only test case.
+ Consider: if add CONST to doSomething2() then test passes.
+ */
+ public void failing_testCallFromLambda() {
+ executeTest(testCasesChecker::checkCallFromLambda, "ClassA.java", "ClassB.java");
+ }
+
+ public void testStaticFactoryMethods() {
+ executeTest(testCasesChecker::checkStaticFactoryMethods, "Cat.java", "Color.java", "Dog.java");
+ }
+
+ public void testStaticFactoryMethodsWeak() {
+ executeTest(testCasesChecker::checkStaticFactoryMethodsWeak, "Cat.java", "Color.java", "Dog.java");
+ }
+
+ @Override
+ protected Algorithm getAlgorithm() {
+ return algorithm;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/TestCasesCheckers.java b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/TestCasesCheckers.java
index afca614e..b720719b 100644
--- a/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/TestCasesCheckers.java
+++ b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/TestCasesCheckers.java
@@ -17,9 +17,11 @@
class TestCasesCheckers {
private static final String CHECK_METHODS_PREFIX = "check";
private final String algorithmName;
+ private boolean isFieldRefactoringEnabled;
- TestCasesCheckers(String algorithmName) {
+ TestCasesCheckers(String algorithmName, boolean isFieldRefactoringEnabled) {
this.algorithmName = algorithmName;
+ this.isFieldRefactoringEnabled = isFieldRefactoringEnabled;
}
@NotNull
@@ -29,10 +31,12 @@ private static String getPackageName() {
return packageName.substring(0, 1).toLowerCase() + packageName.substring(1);
}
- private static void checkStructure(@NotNull RefactoringExecutionContext context, int classes, int methods, int fields) {
+ private void checkStructure(@NotNull RefactoringExecutionContext context, int classes, int methods, int fields) {
assertEquals(classes, context.getClassCount());
assertEquals(methods, context.getMethodsCount());
- assertEquals(fields, context.getFieldsCount());
+ if (isFieldRefactoringEnabled) {
+ assertEquals(fields, context.getFieldsCount());
+ }
}
void checkMoveMethod(@NotNull RefactoringExecutionContext context) {
diff --git a/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/entity/RmmrEntitySearcherTest.java b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/entity/RmmrEntitySearcherTest.java
new file mode 100644
index 00000000..42f8a03d
--- /dev/null
+++ b/src/test/java/org/jetbrains/research/groups/ml_methods/algorithm/entity/RmmrEntitySearcherTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2018 Machine Learning Methods in Software Engineering Group of JetBrains Research
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jetbrains.research.groups.ml_methods.algorithm.entity;
+
+import com.intellij.analysis.AnalysisScope;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+
+public class RmmrEntitySearcherTest extends LightCodeInsightFixtureTestCase {
+ private EntitySearchResult searchResult;
+
+ @Override
+ protected String getTestDataPath() {
+ return "src/test/resources/testCases/" + getPackage();
+ }
+
+ @NotNull
+ @Contract(pure = true)
+ private String getPackage() {
+ return "movieRentalStoreWithFeatureEnvy";
+ }
+
+ public void testAnalyze() {
+ final VirtualFile customer = myFixture.copyFileToProject("Customer.java");
+ final VirtualFile movie = myFixture.copyFileToProject("Movie.java");
+ final VirtualFile rental = myFixture.copyFileToProject("Rental.java");
+
+ AnalysisScope analysisScope = new AnalysisScope(myFixture.getProject(), Arrays.asList(customer, movie, rental));
+ searchResult = RmmrEntitySearcher.analyze(analysisScope);
+
+ assertEquals(3, searchResult.getClasses().size());
+ // TODO: tests tightly depend on RMMR configs, but there is a lot of possible configurations. Rewrite or leave only one config.
+ // checkCustomer();
+ // checkMovie();
+ // checkRental();
+ }
+
+ private void checkCustomer() {
+ Map> expectedConceptualSets = new HashMap<>();
+ expectedConceptualSets.put("Customer.getMovie(Movie)", new HashSet<>(Arrays.asList("Movie", "Rental")));
+ expectedConceptualSets.put("Customer.addRental(Rental)", new HashSet<>(Collections.singletonList("Customer")));
+ expectedConceptualSets.put("Customer.getName()", new HashSet<>(Collections.singletonList("Customer")));
+ checkConceptualSetForClass("Customer", expectedConceptualSets);
+ }
+
+ private void checkMovie() {
+ Map> expectedConceptualSets = new HashMap<>();
+ expectedConceptualSets.put("Movie.getPriceCode()", new HashSet<>(Collections.singletonList("Movie")));
+ expectedConceptualSets.put("Movie.setPriceCode(int)", new HashSet<>(Collections.singletonList("Movie")));
+ expectedConceptualSets.put("Movie.getTitle()", new HashSet<>(Collections.singletonList("Movie")));
+ checkConceptualSetForClass("Movie", expectedConceptualSets);
+ }
+
+ private void checkRental() {
+ Map> expectedConceptualSets = new HashMap<>();
+ expectedConceptualSets.put("Rental.getDaysRented()", new HashSet<>(Collections.singletonList("Rental")));
+ checkConceptualSetForClass("Rental", expectedConceptualSets);
+ }
+
+ private void checkConceptualSetForClass(String className, Map> expectedConceptualSets) {
+ searchResult.getMethods().forEach(methodEntity -> {
+ if (methodEntity.getClassName().equals(className)) {
+ assertTrue(expectedConceptualSets.containsKey(methodEntity.getName()));
+
+ Set conceptualSet = methodEntity.getRelevantProperties().getClasses();
+ Set expectedConceptualSet = expectedConceptualSets.get(methodEntity.getName());
+ assertEquals(expectedConceptualSet, conceptualSet);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/utils/src/main/java/com/sixrr/metrics/utils/MethodUtils.java b/utils/src/main/java/com/sixrr/metrics/utils/MethodUtils.java
index 7790f790..b6ba7eb7 100644
--- a/utils/src/main/java/com/sixrr/metrics/utils/MethodUtils.java
+++ b/utils/src/main/java/com/sixrr/metrics/utils/MethodUtils.java
@@ -1,18 +1,17 @@
/*
- * Copyright 2005-2016 Sixth and Red River Software, Bas Leijdekkers
- * Copyright 2017 Machine Learning Methods in Software Engineering Group of JetBrains Research
+ * Copyright 2018 Machine Learning Methods in Software Engineering Group of JetBrains Research
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
package com.sixrr.metrics.utils;