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 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 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;