-
Notifications
You must be signed in to change notification settings - Fork 1
Implemented draft of RMMR algorithm. #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
174fcd2
6f61018
0fd31dd
bc85cad
df7b1e0
08536b2
538ff85
59b19a2
1fce0bc
79ebe6b
22ac2b6
e227e2b
2d0ddd0
e9f71fd
00333f5
2c4d416
c1379f4
2400fb5
66c2b2c
826ee53
83168b2
9eef351
42a7471
08740ec
ebe7152
b9e8966
28153b7
d2f1015
c0503ef
9f884cd
5cd9df9
82ade8e
bc238ca
a342283
f15f724
9bd97d4
fd94e63
3d86756
afa2096
9fc5780
ac4cf63
bd370e9
c183397
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,9 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <module relativePaths="true" type="JAVA_MODULE" version="4"> | ||
| <component name="NewModuleRootManager" inherit-compiler-output="false"> | ||
| <output url="file://$MODULE_DIR$/build/classes" /> | ||
| <output-test url="file://$MODULE_DIR$/build/testclasses" /> | ||
| <module external.linked.project.id="openapi" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" type="JAVA_MODULE" version="4"> | ||
| <component name="NewModuleRootManager" inherit-compiler-output="true"> | ||
| <exclude-output /> | ||
| <content url="file://$MODULE_DIR$"> | ||
| <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> | ||
| <excludeFolder url="file://$MODULE_DIR$/build" /> | ||
| </content> | ||
| <content url="file://$MODULE_DIR$" /> | ||
| <orderEntry type="inheritedJdk" /> | ||
| <orderEntry type="sourceFolder" forTests="false" /> | ||
| </component> | ||
| </module> | ||
|
|
||
| </module> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| package org.ml_methods_group.algorithm; | ||
|
|
||
| import org.apache.log4j.Logger; | ||
| import org.ml_methods_group.algorithm.entity.ClassEntity; | ||
| import org.ml_methods_group.algorithm.entity.EntitySearchResult; | ||
| import org.ml_methods_group.algorithm.entity.MethodEntity; | ||
| import org.ml_methods_group.config.Logging; | ||
| import org.ml_methods_group.utils.AlgorithmsUtil; | ||
|
|
||
| import java.util.*; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import java.util.stream.Collectors; | ||
| import java.util.stream.Stream; | ||
|
|
||
| public class RMMR extends Algorithm { | ||
| public static final String NAME = "RMMR"; | ||
|
|
||
| private static final Logger LOGGER = Logging.getLogger(MRI.class); | ||
|
|
||
| private final Map<ClassEntity, Set<MethodEntity>> methodsByClass = new HashMap<>(); | ||
| private final List<MethodEntity> units = new ArrayList<>(); | ||
| private final List<ClassEntity> classEntities = new ArrayList<>(); | ||
| private final AtomicInteger progressCount = new AtomicInteger(); | ||
| private ExecutionContext context; | ||
|
|
||
| public RMMR() { | ||
| super(NAME, true); | ||
| } | ||
|
|
||
| @Override | ||
| protected List<Refactoring> calculateRefactorings(ExecutionContext context, boolean enableFieldRefactorings) throws Exception { | ||
| if (enableFieldRefactorings) { | ||
| // TODO: write to LOGGER or throw Exception? Change UI: disable field checkbox if only RMMR is chosen. | ||
| LOGGER.error("Field refactorings are not supported", | ||
| new UnsupportedOperationException("Field refactorings are not supported")); | ||
| } | ||
| this.context = context; | ||
| init(); | ||
| return runParallel(units, context, ArrayList::new, this::findRefactoring, AlgorithmsUtil::combineLists); | ||
| } | ||
|
|
||
| private void init() { | ||
| final EntitySearchResult entities = context.getEntities(); | ||
| LOGGER.info("Init RMMR"); | ||
| units.clear(); | ||
| classEntities.clear(); | ||
|
|
||
| classEntities.addAll(entities.getClasses()); | ||
| units.addAll(entities.getMethods()); | ||
| progressCount.set(0); | ||
|
|
||
| entities.getMethods().forEach(methodEntity -> { | ||
| List<ClassEntity> methodClass = entities.getClasses().stream() | ||
|
||
| .filter(classEntity -> methodEntity.getClassName().equals(classEntity.getName())) | ||
| .collect(Collectors.toList()); | ||
| if (methodClass.size() != 1) { | ||
| LOGGER.error("Found more than 1 class that has this method"); | ||
| } | ||
| methodsByClass.computeIfAbsent(methodClass.get(0), anyKey -> new HashSet<>()).add(methodEntity); | ||
| }); | ||
| } | ||
|
|
||
| private List<Refactoring> findRefactoring(MethodEntity entity, List<Refactoring> accumulator) { | ||
| reportProgress((double) progressCount.incrementAndGet() / units.size(), context); | ||
| context.checkCanceled(); | ||
| if (!entity.isMovable() || classEntities.size() < 2) { | ||
| return accumulator; | ||
| } | ||
| double minDistance = Double.POSITIVE_INFINITY; | ||
| double difference = Double.POSITIVE_INFINITY; | ||
| ClassEntity targetClass = null; | ||
| for (final ClassEntity classEntity : classEntities) { | ||
| final double distance = getDistance(entity, classEntity); | ||
| 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 accuracy = (1 - minDistance) * difference; // TODO: Maybe consider amount of entities? | ||
| if (!targetClassName.equals(entity.getClassName())) { | ||
|
||
| accumulator.add(new Refactoring(entity.getName(), targetClassName, accuracy, entity.isField())); | ||
| } | ||
| return accumulator; | ||
| } | ||
|
|
||
| private double getDistance(MethodEntity methodEntity, ClassEntity classEntity) { | ||
| int number = 0; | ||
| double sumOfDistances = 0; | ||
|
|
||
| if (methodsByClass.containsKey(classEntity)) { | ||
| for (MethodEntity methodEntityInClass : methodsByClass.get(classEntity)) { | ||
| if (!methodEntity.equals(methodEntityInClass)) { | ||
| sumOfDistances += getDistance(methodEntity, methodEntityInClass); | ||
| number++; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return number == 0 ? 1 : sumOfDistances / number; | ||
| } | ||
|
|
||
| private double getDistance(MethodEntity methodEntity1, MethodEntity methodEntity2) { | ||
| // TODO: Maybe add to methodEntity2 source class where it is located? | ||
| int sizeOfIntersection = intersection(methodEntity1.getRelevantProperties().getClasses(), | ||
| methodEntity2.getRelevantProperties().getClasses()).size(); | ||
|
||
| int sizeOfUnion = union(methodEntity1.getRelevantProperties().getClasses(), | ||
| methodEntity2.getRelevantProperties().getClasses()).size(); | ||
| return sizeOfIntersection == 0 ? 1 : 1 - (double) sizeOfIntersection / sizeOfUnion; | ||
|
||
| } | ||
|
|
||
| private <T> Set<T> intersection(Set<T> set1, Set<T> set2) { | ||
| Set<T> intersection = new HashSet<>(set1); | ||
| intersection.retainAll(set2); | ||
| return intersection; | ||
| } | ||
|
|
||
| private <T> Set<T> union(Set<T> set1, Set<T> set2) { | ||
| Set<T> union = new HashSet<>(set1); | ||
| union.addAll(set2); | ||
| return union; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| package org.ml_methods_group.algorithm.entity; | ||
|
|
||
| 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 org.apache.log4j.Logger; | ||
| import org.jetbrains.annotations.Contract; | ||
| import org.jetbrains.annotations.NotNull; | ||
| import org.jetbrains.annotations.Nullable; | ||
| import org.ml_methods_group.algorithm.properties.finder_strategy.FinderStrategy; | ||
| import org.ml_methods_group.algorithm.properties.finder_strategy.RmmrStrategy; | ||
| import org.ml_methods_group.config.Logging; | ||
|
|
||
| import java.util.*; | ||
|
|
||
| public class RmmrEntitySearcher { | ||
| private static final Logger LOGGER = Logging.getLogger(EntitySearcher.class); | ||
|
|
||
| private final Map<String, PsiClass> classForName = new HashMap<>(); | ||
| private final Map<PsiMethod, MethodEntity> entities = new HashMap<>(); | ||
| private final Map<PsiClass, ClassEntity> classEntities = new HashMap<>(); | ||
| private final AnalysisScope scope; | ||
| private final long startTime; | ||
| private final FinderStrategy strategy; | ||
| private final ProgressIndicator indicator; | ||
|
|
||
| private RmmrEntitySearcher(AnalysisScope scope) { | ||
| this.scope = scope; | ||
| strategy = RmmrStrategy.getInstance(); | ||
| startTime = System.currentTimeMillis(); | ||
| if (ProgressManager.getInstance().hasProgressIndicator()) { | ||
| indicator = ProgressManager.getInstance().getProgressIndicator(); | ||
| } else { | ||
| indicator = new EmptyProgressIndicator(); | ||
| } | ||
| } | ||
|
|
||
| @NotNull | ||
| public static EntitySearchResult analyze(AnalysisScope scope) { | ||
| final RmmrEntitySearcher finder = new RmmrEntitySearcher(scope); | ||
| return finder.runCalculations(); | ||
| } | ||
|
|
||
| @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()); | ||
| indicator.popState(); | ||
| return prepareResult(); | ||
| } | ||
|
|
||
| @NotNull | ||
| private EntitySearchResult prepareResult() { | ||
| LOGGER.info("Preparing results..."); | ||
| final List<ClassEntity> classes = new ArrayList<>(); | ||
| final List<MethodEntity> methods = new ArrayList<>(); | ||
| for (MethodEntity methodEntity : entities.values()) { | ||
| indicator.checkCanceled(); | ||
| methods.add(methodEntity); | ||
| } | ||
| for (ClassEntity 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); | ||
| } | ||
|
|
||
|
|
||
| private class UnitsFinder extends JavaRecursiveElementVisitor { | ||
| @Override | ||
| public void visitFile(PsiFile file) { | ||
| indicator.checkCanceled(); | ||
| if (strategy.acceptFile(file)) { | ||
| LOGGER.info("Indexing " + file.getName()); | ||
|
||
| super.visitFile(file); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void visitClass(PsiClass aClass) { | ||
| indicator.checkCanceled(); | ||
| //classForName.put(getHumanReadableName(aClass), aClass); | ||
| //TODO: maybe qualified name? Otherwise name collision may occur. | ||
| classForName.put(aClass.getName(), aClass); | ||
| if (!strategy.acceptClass(aClass)) { | ||
| return; | ||
| } | ||
| classEntities.put(aClass, new ClassEntity(aClass)); | ||
| super.visitClass(aClass); | ||
| } | ||
|
|
||
| @Override | ||
| public void visitMethod(PsiMethod method) { | ||
| indicator.checkCanceled(); | ||
| if (!strategy.acceptMethod(method)) { | ||
| return; | ||
| } | ||
| entities.put(method, new MethodEntity(method)); | ||
| super.visitMethod(method); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| // 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; | ||
|
|
||
| private MethodEntity currentMethod; | ||
|
|
||
| @Override | ||
| public void visitMethod(PsiMethod method) { | ||
| indicator.checkCanceled(); | ||
| final MethodEntity methodEntity = entities.get(method); | ||
| if (methodEntity == null) { | ||
| super.visitMethod(method); | ||
| return; | ||
| } | ||
| final RelevantProperties methodProperties = methodEntity.getRelevantProperties(); | ||
| if (currentMethod == null) { | ||
| currentMethod = methodEntity; | ||
| } | ||
|
|
||
| for (PsiParameter attribute : method.getParameterList().getParameters()) { | ||
| PsiType attributeType = attribute.getType(); | ||
| if (attributeType instanceof PsiClassType) { | ||
| String className = ((PsiClassType) attributeType).getClassName(); | ||
| if (isClassInScope(className)) { | ||
| methodProperties.addClass(classForName.get(className)); | ||
| } | ||
| } | ||
| } | ||
| super.visitMethod(method); | ||
| if (currentMethod == methodEntity) { | ||
| currentMethod = null; | ||
| } | ||
| reportPropertiesCalculated(); | ||
| } | ||
|
|
||
| @Override | ||
| public void visitNewExpression(PsiNewExpression expression) { | ||
| indicator.checkCanceled(); | ||
| String className = null; | ||
| PsiType type = expression.getType(); | ||
| if (type instanceof PsiClassType) { | ||
| className = ((PsiClassType) expression.getType()).getClassName(); | ||
|
||
| } | ||
| final PsiClass usedClass = classForName.get(className); | ||
| if (currentMethod != null && className != null && isClassInScope(usedClass)) { | ||
| currentMethod.getRelevantProperties().addClass(usedClass); | ||
| } | ||
| super.visitNewExpression(expression); | ||
| } | ||
|
|
||
| @Override | ||
| public void visitMethodCallExpression(PsiMethodCallExpression expression) { | ||
| // Do not find constructors. It does not consider them as method calls. | ||
| indicator.checkCanceled(); | ||
| final PsiMethod called = expression.resolveMethod(); | ||
| final PsiClass usedClass = called != null ? called.getContainingClass() : null; | ||
| if (currentMethod != null && called != null && isClassInScope(usedClass)) { | ||
| currentMethod.getRelevantProperties().addClass(usedClass); | ||
| } | ||
| super.visitMethodCallExpression(expression); | ||
| } | ||
|
|
||
| private void reportPropertiesCalculated() { | ||
| propertiesCalculated++; | ||
| if (indicator != null) { | ||
| indicator.setFraction((double) propertiesCalculated / entities.size()); | ||
| } | ||
| } | ||
|
|
||
| @Contract(pure = true) | ||
| private boolean isClassInScope(String aClass) { | ||
| return classForName.containsKey(aClass); | ||
| } | ||
|
|
||
| @Contract("null -> false") | ||
| private boolean isClassInScope(final @Nullable PsiClass aClass) { | ||
| return aClass != null && classForName.containsKey(aClass.getName()); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Corrected.