diff --git a/build.gradle.kts b/build.gradle.kts index 2b92afd..046922b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation("org.ow2.asm:asm-tree:9.5") implementation("org.ow2.asm:asm-analysis:9.5") implementation("org.ow2.asm:asm-util:9.5") + implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0") testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/output.txt b/output.txt new file mode 100644 index 0000000..46c6bca --- /dev/null +++ b/output.txt @@ -0,0 +1,46 @@ +[ { + "metricType" : "MAX_DEPTH_COUNT", + "items" : [ { + "description" : "Max depth", + "value" : "P1: 2" + } ] +}, { + "metricType" : "AVERAGE_DEPTH_COUNT", + "items" : [ { + "description" : "Average depth", + "value" : "1.5" + } ] +}, { + "metricType" : "AVERAGE_OVERRIDES_COUNT", + "items" : [ { + "description" : "getX", + "value" : "0" + }, { + "description" : "getY", + "value" : "0" + }, { + "description" : "box", + "value" : "0" + }, { + "description" : "", + "value" : "0" + } ] +}, { + "metricType" : "AVERAGE_FIELDS_COUNT", + "items" : [ { + "description" : "Average fields amount", + "value" : "9.333333333333334" + } ] +}, { + "metricType" : "ABC_COUNT", + "items" : [ { + "description" : "Average assignments count", + "value" : "6.0" + }, { + "description" : "Average branches count", + "value" : "5.375" + }, { + "description" : "Average conditions count", + "value" : "7.0" + } ] +} ] diff --git a/src/main/java/org/example/Example.java b/src/main/java/org/example/Example.java index 52d0abe..78df736 100644 --- a/src/main/java/org/example/Example.java +++ b/src/main/java/org/example/Example.java @@ -11,8 +11,6 @@ public class Example { public static void main(String[] args) throws IOException { -// var printer = new ByteCodePrinter(); -// printer.printBubbleSortBytecode(); try (JarFile sampleJar = new JarFile("src/main/resources/sample.jar")) { Enumeration enumeration = sampleJar.entries(); diff --git a/src/main/java/org/example/LabMain.java b/src/main/java/org/example/LabMain.java new file mode 100644 index 0000000..bc8d335 --- /dev/null +++ b/src/main/java/org/example/LabMain.java @@ -0,0 +1,84 @@ +package org.example; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.example.visitor.AbcVisitor; +import org.example.visitor.ClassMapVisitor; +import org.example.workers.*; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +public class LabMain { + + private static final String PATH_TO_JAR = "src/main/resources/sample.jar"; + + public static void main(String[] args) throws IOException { + boolean toFile; + try { + toFile = Boolean.parseBoolean(args[0]); + } catch (Exception e) { + toFile = false; + } + + ClassMapVisitor classMapVisitor = new ClassMapVisitor(); + AbcVisitor abcVisitor = new AbcVisitor(); + + PrintStream ps = null; + try { + OutputStream os; + if (toFile) { + os = new FileOutputStream("output.txt"); + } else { + os = System.out; + } + + ps = new PrintStream(os); + } catch (IOException e) { + e.printStackTrace(); + } + + assert ps != null; + List resultStats = new ArrayList<>(); + + MaxDepthCounterWorker maxDepthCounterWorker = new MaxDepthCounterWorker(); + StatItemDto maxDepthCounterWorkerStat = maxDepthCounterWorker.doTheJob(PATH_TO_JAR, classMapVisitor); + resultStats.add(maxDepthCounterWorkerStat); + + AverageDepthCounterWorker averageDepthCounterWorker = new AverageDepthCounterWorker(); + StatItemDto averageDepthCounterWorkerStat = averageDepthCounterWorker.doTheJob(PATH_TO_JAR, classMapVisitor); + resultStats.add(averageDepthCounterWorkerStat); + + AverageOverridesCounterWorker averageOverridesCounterWorker = new AverageOverridesCounterWorker(); + StatItemDto averageOverridesCounterWorkerStat = averageOverridesCounterWorker.doTheJob(PATH_TO_JAR, classMapVisitor); + resultStats.add(averageOverridesCounterWorkerStat); + + AverageFieldsCountWorker averageFieldsCountWorker = new AverageFieldsCountWorker(); + StatItemDto averageFieldsCountWorkerStat = averageFieldsCountWorker.doTheJob(PATH_TO_JAR, classMapVisitor); + resultStats.add(averageFieldsCountWorkerStat); + + AbcWorker abcWorker = new AbcWorker(); + StatItemDto abcWorkerStat = abcWorker.doTheJob(PATH_TO_JAR, abcVisitor); + resultStats.add(abcWorkerStat); + + printAsJson(resultStats, ps); + + if (toFile) { + ps.close(); + } + } + + private static void printAsJson(List list, PrintStream out) { + try { + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(list); + out.println(json); + } catch (Exception e) { + e.printStackTrace(out); + } + } +} diff --git a/src/main/java/org/example/StatItemDto.java b/src/main/java/org/example/StatItemDto.java new file mode 100644 index 0000000..5495014 --- /dev/null +++ b/src/main/java/org/example/StatItemDto.java @@ -0,0 +1,43 @@ +package org.example; + +import org.example.workers.MetricEnum; + +import java.util.List; + +public class StatItemDto { + + public static class Item { + + private final String description; + private final String value; + + public Item(String description, String value) { + this.description = description; + this.value = value; + } + + public String getDescription() { + return description; + } + + public String getValue() { + return value; + } + } + + private final MetricEnum metricType; + private final List items; + + public StatItemDto(MetricEnum metricType, List items) { + this.metricType = metricType; + this.items = items; + } + + public MetricEnum getMetricType() { + return metricType; + } + + public List getItems() { + return items; + } +} diff --git a/src/main/java/org/example/visitor/AbcVisitor.java b/src/main/java/org/example/visitor/AbcVisitor.java new file mode 100644 index 0000000..76692de --- /dev/null +++ b/src/main/java/org/example/visitor/AbcVisitor.java @@ -0,0 +1,110 @@ +package org.example.visitor; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.util.HashMap; +import java.util.Map; + +public class AbcVisitor extends ClassVisitor { + + private String className; + + private final Map assignmentCount = new HashMap<>(); + private final Map branchCount = new HashMap<>(); + private final Map conditionCount = new HashMap<>(); + + public AbcVisitor() { + super(Opcodes.ASM9); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + className = name; + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + return new MethodVisitor(Opcodes.ASM9, mv) { + + @Override + public void visitVarInsn(int opcode, int var) { + if ((opcode >= Opcodes.ISTORE && opcode <= 78) || opcode == Opcodes.IINC) { + assignmentCount.merge(className, 1, Integer::sum); + } + super.visitVarInsn(opcode, var); + } + + @Override + public void visitIincInsn(int var, int increment) { + assignmentCount.merge(className, 1, Integer::sum); + super.visitIincInsn(var, increment); + } + + // ------------------------------ + // B — Branches + // ------------------------------ + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + branchCount.merge(className, 1, Integer::sum); + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + + @Override + public void visitTypeInsn(int opcode, String type) { + if (opcode == Opcodes.NEW) { + branchCount.merge(className, 1, Integer::sum); + } + super.visitTypeInsn(opcode, type); + } + + + @Override + public void visitJumpInsn(int opcode, Label label) { + if (opcode >= Opcodes.IFEQ && opcode <= Opcodes.IF_ACMPNE) { + conditionCount.merge(className, 1, Integer::sum); + } + + if (opcode == Opcodes.GOTO) { + conditionCount.merge(className, 1, Integer::sum); + } + + super.visitJumpInsn(opcode, label); + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + conditionCount.merge(className, 1, Integer::sum); + super.visitLookupSwitchInsn(dflt, keys, labels); + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { + conditionCount.merge(className, 1, Integer::sum); + super.visitTableSwitchInsn(min, max, dflt, labels); + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + conditionCount.merge(className, 1, Integer::sum); + super.visitTryCatchBlock(start, end, handler, type); + } + }; + } + + public Map getAssignmentCount() { + return assignmentCount; + } + + public Map getBranchCount() { + return branchCount; + } + + public Map getConditionCount() { + return conditionCount; + } +} diff --git a/src/main/java/org/example/visitor/ClassMapVisitor.java b/src/main/java/org/example/visitor/ClassMapVisitor.java new file mode 100644 index 0000000..dff9ddf --- /dev/null +++ b/src/main/java/org/example/visitor/ClassMapVisitor.java @@ -0,0 +1,56 @@ +package org.example.visitor; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.objectweb.asm.Opcodes.ASM8; + +public class ClassMapVisitor extends ClassVisitor { + + private final Map superMap = new HashMap<>(); + + private final Map fieldCount = new HashMap<>(); + + private final Map> methods = new HashMap<>(); + + public ClassMapVisitor() { + super(ASM8); + } + + public Map getSuperMap() { + return superMap; + } + + public Map getFieldCount() { + return fieldCount; + } + + public Map> getMethods() { + return methods; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + superMap.put(name, superName); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + fieldCount.merge(name, 1, Integer::sum); + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + methods.computeIfAbsent(name, k -> new ArrayList<>()) + .add(name + descriptor); + + return super.visitMethod(access, name, descriptor, signature, exceptions); + } +} diff --git a/src/main/java/org/example/workers/AbcWorker.java b/src/main/java/org/example/workers/AbcWorker.java new file mode 100644 index 0000000..5e88106 --- /dev/null +++ b/src/main/java/org/example/workers/AbcWorker.java @@ -0,0 +1,50 @@ +package org.example.workers; + +import org.example.StatItemDto; +import org.example.visitor.AbcVisitor; +import org.objectweb.asm.ClassVisitor; + +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class AbcWorker extends BaseWorker { + + @Override + public StatItemDto doTheJob(String pathToJar, ClassVisitor visitor) throws IOException { + loadJar(pathToJar, visitor); + return collectAbc(visitor); + } + + private StatItemDto collectAbc(ClassVisitor visitor) { + Map assignmentCount = ((AbcVisitor) visitor).getAssignmentCount(); + double avgAssignments = assignmentCount.values() + .stream() + .mapToInt(i -> i) + .average() + .orElse(0.0); + + Map branchCount = ((AbcVisitor) visitor).getBranchCount(); + double avgBranches = branchCount.values() + .stream() + .mapToInt(i -> i) + .average() + .orElse(0.0); + + Map conditionCount = ((AbcVisitor) visitor).getConditionCount(); + double avgConditions = conditionCount.values() + .stream() + .mapToInt(i -> i) + .average() + .orElse(0.0); + + List items = new ArrayList<>(); + items.add(new StatItemDto.Item("Average assignments count", Double.toString(avgAssignments))); + items.add(new StatItemDto.Item("Average branches count", Double.toString(avgBranches))); + items.add(new StatItemDto.Item("Average conditions count", Double.toString(avgConditions))); + + return new StatItemDto(MetricEnum.ABC_COUNT, items); + } +} diff --git a/src/main/java/org/example/workers/AverageDepthCounterWorker.java b/src/main/java/org/example/workers/AverageDepthCounterWorker.java new file mode 100644 index 0000000..ea9b3f2 --- /dev/null +++ b/src/main/java/org/example/workers/AverageDepthCounterWorker.java @@ -0,0 +1,52 @@ +package org.example.workers; + +import org.example.StatItemDto; +import org.example.visitor.ClassMapVisitor; +import org.objectweb.asm.ClassVisitor; + +import java.io.IOException; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AverageDepthCounterWorker extends BaseWorker { + + @Override + public StatItemDto doTheJob(String pathToJar, ClassVisitor visitor) throws IOException { + loadJar(pathToJar, visitor); + Map classMap = ((ClassMapVisitor) visitor).getSuperMap(); + Map depths = computeDepths(classMap); + + return collectAverageDepth(depths); + } + + private Map computeDepths(Map classMap) { + Map result = new HashMap<>(); + for (String clazz : classMap.keySet()) { + result.put(clazz, depthOf(clazz, classMap)); + } + return result; + } + + private int depthOf(String cls, Map classMap) { + int depth = 0; + String current = cls; + + while (true) { + String parent = classMap.get(current); + if (parent == null) return depth; + depth++; + current = parent; + } + } + + private StatItemDto collectAverageDepth(Map depths) { + double average = depths.values().stream() + .mapToDouble(i -> i) + .average() + .orElse(0); + return new StatItemDto(MetricEnum.AVERAGE_DEPTH_COUNT, List.of(new StatItemDto.Item("Average depth", Double.toString(average)))); + } +} diff --git a/src/main/java/org/example/workers/AverageFieldsCountWorker.java b/src/main/java/org/example/workers/AverageFieldsCountWorker.java new file mode 100644 index 0000000..115b7f0 --- /dev/null +++ b/src/main/java/org/example/workers/AverageFieldsCountWorker.java @@ -0,0 +1,31 @@ +package org.example.workers; + +import org.example.StatItemDto; +import org.example.visitor.ClassMapVisitor; +import org.objectweb.asm.ClassVisitor; + +import java.io.IOException; +import java.io.PrintStream; +import java.util.List; +import java.util.Map; + +public class AverageFieldsCountWorker extends BaseWorker { + + @Override + public StatItemDto doTheJob(String pathToJar, ClassVisitor visitor) throws IOException { + loadJar(pathToJar, visitor); + Map fieldCount = ((ClassMapVisitor) visitor).getFieldCount(); + + return collectAverageFieldsCount(fieldCount); + } + + private StatItemDto collectAverageFieldsCount(Map fieldCount) { + double avgFields = fieldCount.values() + .stream() + .mapToInt(i -> i) + .average() + .orElse(0); + + return new StatItemDto(MetricEnum.AVERAGE_FIELDS_COUNT, List.of(new StatItemDto.Item("Average fields amount", Double.toString(avgFields)))); + } +} diff --git a/src/main/java/org/example/workers/AverageOverridesCounterWorker.java b/src/main/java/org/example/workers/AverageOverridesCounterWorker.java new file mode 100644 index 0000000..ab790c1 --- /dev/null +++ b/src/main/java/org/example/workers/AverageOverridesCounterWorker.java @@ -0,0 +1,54 @@ +package org.example.workers; + +import org.example.StatItemDto; +import org.example.visitor.ClassMapVisitor; +import org.objectweb.asm.ClassVisitor; + +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AverageOverridesCounterWorker extends BaseWorker { + + private final Map overriddenCount = new HashMap<>(); + + @Override + public StatItemDto doTheJob(String pathToJar, ClassVisitor visitor) throws IOException { + loadJar(pathToJar, visitor); + Map> methods = ((ClassMapVisitor) visitor).getMethods(); + Map superMap = ((ClassMapVisitor) visitor).getSuperMap(); + for (String cls : methods.keySet()) { + int count = 0; + String current = superMap.get(cls); + + while (current != null) { + + List parentMethods = methods.get(current); + if (parentMethods != null) { + for (String m : methods.get(cls)) { + if (parentMethods.contains(m)) { + count++; + } + } + } + + current = superMap.get(current); + } + + overriddenCount.put(cls, count); + } + return collectOverriddenCounts(); + } + + private StatItemDto collectOverriddenCounts() { + List items = new ArrayList<>(); + + for (String cls : overriddenCount.keySet()) { + items.add(new StatItemDto.Item(cls, Integer.toString(overriddenCount.get(cls)))); + } + return new StatItemDto(MetricEnum.AVERAGE_OVERRIDES_COUNT, items); + } +} diff --git a/src/main/java/org/example/workers/BaseWorker.java b/src/main/java/org/example/workers/BaseWorker.java new file mode 100644 index 0000000..3da26a7 --- /dev/null +++ b/src/main/java/org/example/workers/BaseWorker.java @@ -0,0 +1,28 @@ +package org.example.workers; + +import org.example.StatItemDto; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; + +import java.io.*; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; + +abstract public class BaseWorker { + + abstract public StatItemDto doTheJob(String pathToJar, ClassVisitor visitor) throws IOException; + + void loadJar(String jarPath, ClassVisitor visitor) throws IOException { + try (InputStream in = new FileInputStream(jarPath); + JarInputStream jar = new JarInputStream(in)) { + + JarEntry entry; + while ((entry = jar.getNextJarEntry()) != null) { + if (!entry.getName().endsWith(".class")) continue; + + ClassReader cr = new ClassReader(jar); + cr.accept(visitor, 0); + } + } + } +} diff --git a/src/main/java/org/example/workers/MaxDepthCounterWorker.java b/src/main/java/org/example/workers/MaxDepthCounterWorker.java new file mode 100644 index 0000000..981eedf --- /dev/null +++ b/src/main/java/org/example/workers/MaxDepthCounterWorker.java @@ -0,0 +1,57 @@ +package org.example.workers; + +import org.example.StatItemDto; +import org.example.visitor.ClassMapVisitor; +import org.objectweb.asm.ClassVisitor; + +import java.io.IOException; +import java.io.PrintStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MaxDepthCounterWorker extends BaseWorker { + + @Override + public StatItemDto doTheJob(String pathToJar, ClassVisitor visitor) throws IOException { + loadJar(pathToJar, visitor); + Map classMap = ((ClassMapVisitor) visitor).getSuperMap(); + Map depths = computeDepths(classMap); + + return collectMaxDepth(depths); + } + + private Map computeDepths(Map classMap) { + Map result = new HashMap<>(); + for (String clazz : classMap.keySet()) { + result.put(clazz, depthOf(clazz, classMap)); + } + return result; + } + + private int depthOf(String cls, Map classMap) { + int depth = 0; + String current = cls; + + while (true) { + String parent = classMap.get(current); + if (parent == null) return depth; + depth++; + current = parent; + } + } + + private StatItemDto collectMaxDepth(Map depths) { + Map.Entry maxEntry = depths.entrySet() + .stream() + .max(Map.Entry.comparingByValue()) + .orElse(null); + + if (maxEntry != null) { + return new StatItemDto(MetricEnum.MAX_DEPTH_COUNT, List.of(new StatItemDto.Item("Max depth", maxEntry.getKey() + ": " + maxEntry.getValue()))); + } + + return new StatItemDto(MetricEnum.MAX_DEPTH_COUNT, Collections.emptyList()); + } +} diff --git a/src/main/java/org/example/workers/MetricEnum.java b/src/main/java/org/example/workers/MetricEnum.java new file mode 100644 index 0000000..c87e21d --- /dev/null +++ b/src/main/java/org/example/workers/MetricEnum.java @@ -0,0 +1,10 @@ +package org.example.workers; + +public enum MetricEnum { + + AVERAGE_DEPTH_COUNT, + AVERAGE_FIELDS_COUNT, + AVERAGE_OVERRIDES_COUNT, + MAX_DEPTH_COUNT, + ABC_COUNT +}