diff --git a/README.md b/README.md index e3d9c8a..2486cde 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/9A22t-SS) Разработать standalone приложение, которое имеет следующие возможности: Принимает на вход проект в виде .jar файла diff --git a/build.gradle.kts b/build.gradle.kts index 2b92afd..b63d98f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,25 +1,26 @@ plugins { - id("java") + id("java") } group = "org.example" version = "1.0-SNAPSHOT" repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation("org.ow2.asm:asm:9.5") - 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.16.1") + implementation("org.ow2.asm:asm:9.5") + implementation("org.ow2.asm:asm-tree:9.5") + implementation("org.ow2.asm:asm-analysis:9.5") + implementation("org.ow2.asm:asm-util:9.5") - testImplementation(platform("org.junit:junit-bom:5.10.0")) - testImplementation("org.junit.jupiter:junit-jupiter") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } tasks.test { - useJUnitPlatform() + useJUnitPlatform() } \ No newline at end of file diff --git a/src/main/java/org/example/Example.java b/src/main/java/org/example/Example.java deleted file mode 100644 index 52d0abe..0000000 --- a/src/main/java/org/example/Example.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.example; - -import org.example.visitor.ClassPrinter; -import org.objectweb.asm.ClassReader; - -import java.io.IOException; -import java.util.Enumeration; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -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(); - - while (enumeration.hasMoreElements()) { - JarEntry entry = enumeration.nextElement(); - if (entry.getName().endsWith(".class")) { - ClassPrinter cp = new ClassPrinter(); - ClassReader cr = new ClassReader(sampleJar.getInputStream(entry)); - cr.accept(cp, 0); - } - } - } - } -} diff --git a/src/main/java/org/example/JarAnalyzer.java b/src/main/java/org/example/JarAnalyzer.java new file mode 100644 index 0000000..0fc6460 --- /dev/null +++ b/src/main/java/org/example/JarAnalyzer.java @@ -0,0 +1,79 @@ +package org.example; + +import org.example.visitor.*; +import org.objectweb.asm.ClassReader; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class JarAnalyzer { + + public static void main(String[] args) throws IOException { + PrettyPrinter printer = new PrettyPrinter(); + MetricsCounter metricsCounter = new MetricsCounter(); + Map> graph = new HashMap<>(); + Map classesByNames = new HashMap<>(); + var abcMetrics = new MethodMetrics(0, 0, 0); + var nClasses = 0; + var classFields = 0L; + + try (JarFile sampleJar = new JarFile("src/main/resources/sample.jar")) { + Enumeration enumeration = sampleJar.entries(); + + while (enumeration.hasMoreElements()) { + JarEntry entry = enumeration.nextElement(); + if (entry.getName().endsWith(".class")) { + var classInfo = getClassInfo(sampleJar, entry, classesByNames, graph); + abcMetrics = abcMetrics.merge(classInfo.methodMetrics()); + if (!classInfo.isInterface()) { + nClasses++; + classFields += classInfo.fields().size(); + } + } + } + + var outputMetrics = new OutputMetrics( + metricsCounter.maxDepth(graph), + metricsCounter.averageDepth(graph), + (double) classFields / nClasses, + metricsCounter.overridenMethodsCount(graph, classesByNames), + abcMetrics, + abcMetrics.abc() + ); + + var reportFile = new File("metrics/metrics.json"); + printer.print(outputMetrics, System.out, new FileOutputStream(reportFile)); + } + } + + private static ClassInfo getClassInfo( + JarFile sampleJar, + JarEntry entry, + Map classesByNames, + Map> graph + ) throws IOException { + ClassAnalyzer cp = new ClassAnalyzer(); + ClassReader cr = new ClassReader(sampleJar.getInputStream(entry)); + cr.accept(cp, 0); + var classInfo = cp.toClassInfo(); + classesByNames.put(classInfo.name(), classInfo); + addInheritanceEdges(graph, classInfo); + return classInfo; + } + + private static void addInheritanceEdges(Map> graph, ClassInfo classInfo) { + graph.computeIfAbsent( + classInfo.superName(), + k -> new ArrayList<>()).add(classInfo); + classInfo.interfaces().forEach(i -> { + graph.computeIfAbsent(i, k -> new ArrayList<>()).add(classInfo); + }); + } +} diff --git a/src/main/java/org/example/visitor/ClassAnalyzer.java b/src/main/java/org/example/visitor/ClassAnalyzer.java new file mode 100644 index 0000000..084a94d --- /dev/null +++ b/src/main/java/org/example/visitor/ClassAnalyzer.java @@ -0,0 +1,69 @@ +package org.example.visitor; + +import org.objectweb.asm.*; + +import java.util.*; + +import static org.objectweb.asm.Opcodes.*; + +public class ClassAnalyzer extends ClassVisitor { + private String name; + private String superName; + private Boolean isInterface; + private HashSet interfaces; + private final List methods = new ArrayList<>(); + private final List fields = new ArrayList<>(); + private final MethodAnalyzer methodAnalyzer = new MethodAnalyzer(ASM8); + + public ClassAnalyzer() { + super(ASM8); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + this.name = name; + this.superName = superName; + this.interfaces = new HashSet<>(List.of(interfaces)); + this.isInterface = (access & Opcodes.ACC_INTERFACE) != 0; + + var interfacesAsString = String.join(", ", interfaces); + if (!interfacesAsString.isEmpty()) { + interfacesAsString = "implements " + interfacesAsString; + } + System.out.printf("\n %s extends %s %s {%n", name, superName, interfacesAsString); + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { + System.out.println(" " + desc + " " + name); + fields.add(name); + return null; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + System.out.println(" " + name + desc); + if (!Objects.equals(name, "")) { + methods.add(new MethodSignature(name, desc)); + } + return methodAnalyzer; + } + + @Override + public void visitEnd() { + System.out.println("}"); + } + + public ClassInfo toClassInfo() { + return new ClassInfo( + name, + isInterface, + superName, + interfaces, + methods, + fields, + methodAnalyzer.methodMetrics() + ); + } +} + diff --git a/src/main/java/org/example/visitor/ClassInfo.java b/src/main/java/org/example/visitor/ClassInfo.java new file mode 100644 index 0000000..a7caf98 --- /dev/null +++ b/src/main/java/org/example/visitor/ClassInfo.java @@ -0,0 +1,14 @@ +package org.example.visitor; + +import java.util.HashSet; +import java.util.List; + +public record ClassInfo ( + String name, + Boolean isInterface, + String superName, + HashSet interfaces, + List methods, + List fields, + MethodMetrics methodMetrics +) {} diff --git a/src/main/java/org/example/visitor/ClassPrinter.java b/src/main/java/org/example/visitor/ClassPrinter.java deleted file mode 100644 index ba1ab9f..0000000 --- a/src/main/java/org/example/visitor/ClassPrinter.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.example.visitor; - -import org.objectweb.asm.*; - -import static org.objectweb.asm.Opcodes.ASM8; - -public class ClassPrinter extends ClassVisitor { - public ClassPrinter() { - super(ASM8); - } - - public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { - System.out.println("\n" + name + " extends " + superName + " {"); - } - - public void visitSource(String source, String debug) { - } - - public void visitOuterClass(String owner, String name, String desc) { - } - - public AnnotationVisitor visitAnnotation(String desc, boolean visible) { - return null; - } - - public void visitAttribute(Attribute attr) { - } - - public void visitInnerClass(String name, String outerName, String innerName, int access) { - } - - public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { - System.out.println(" " + desc + " " + name); - return null; - } - - public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { - System.out.println(" " + name + desc); - return null; - } - - public void visitEnd() { - System.out.println("}"); - } -} - diff --git a/src/main/java/org/example/visitor/Leaf.java b/src/main/java/org/example/visitor/Leaf.java new file mode 100644 index 0000000..4993507 --- /dev/null +++ b/src/main/java/org/example/visitor/Leaf.java @@ -0,0 +1,4 @@ +package org.example.visitor; + +public record Leaf(String className, int depth) { +} diff --git a/src/main/java/org/example/visitor/MethodAnalyzer.java b/src/main/java/org/example/visitor/MethodAnalyzer.java new file mode 100644 index 0000000..aefafcc --- /dev/null +++ b/src/main/java/org/example/visitor/MethodAnalyzer.java @@ -0,0 +1,78 @@ +package org.example.visitor; + +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; + +import java.util.List; + +import static org.objectweb.asm.Opcodes.*; + +public class MethodAnalyzer extends MethodVisitor { + int assignments; + int conditionals; + int invocations; + + public MethodAnalyzer(int api) { + super(api); + } + + @Override + public void visitVarInsn(int opcode, int var) { + if (List.of(ISTORE, LSTORE, FSTORE, DSTORE, ASTORE).contains(opcode)) { + assignments++; + } + super.visitVarInsn(opcode, var); + } + + @Override + public void visitIntInsn(int opcode, int operand) { + if (opcode == NEWARRAY) { + invocations++; + } + super.visitIntInsn(opcode, operand); + } + + @Override + public void visitTypeInsn(int opcode, String type) { + if (List.of(NEW, NEWARRAY).contains(opcode)) { + invocations++; + } + super.visitTypeInsn(opcode, type); + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + conditionals++; + super.visitJumpInsn(opcode, label); + } + + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + invocations++; + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + + @Override + public void visitTryCatchBlock(org.objectweb.asm.Label start, org.objectweb.asm.Label end, org.objectweb.asm.Label + handler, String type) { + conditionals++; + super.visitTryCatchBlock(start, end, handler, type); + } + + @Override + public void visitLookupSwitchInsn(org.objectweb.asm.Label dflt, int[] keys, Label[] labels) { + conditionals += keys.length; + super.visitLookupSwitchInsn(dflt, keys, labels); + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, org.objectweb.asm.Label... labels) { + conditionals += labels.length; + super.visitTableSwitchInsn(min, max, dflt, labels); + } + + public MethodMetrics methodMetrics() { + return new MethodMetrics(assignments, invocations, conditionals); + } +} diff --git a/src/main/java/org/example/visitor/MethodMetrics.java b/src/main/java/org/example/visitor/MethodMetrics.java new file mode 100644 index 0000000..56b5121 --- /dev/null +++ b/src/main/java/org/example/visitor/MethodMetrics.java @@ -0,0 +1,20 @@ +package org.example.visitor; + +public record MethodMetrics( + int assignments, + int branches, + int conditionals +) { + + public MethodMetrics merge(MethodMetrics other) { + return new MethodMetrics( + assignments + other.assignments, + branches + other.branches, + conditionals + other.conditionals + ); + } + + public double abc() { + return Math.sqrt(assignments * assignments + branches * branches + conditionals * conditionals); + } +} diff --git a/src/main/java/org/example/visitor/MethodSignature.java b/src/main/java/org/example/visitor/MethodSignature.java new file mode 100644 index 0000000..b5fbb87 --- /dev/null +++ b/src/main/java/org/example/visitor/MethodSignature.java @@ -0,0 +1,3 @@ +package org.example.visitor; + +public record MethodSignature(String name, String desc) {} diff --git a/src/main/java/org/example/visitor/MetricsCounter.java b/src/main/java/org/example/visitor/MetricsCounter.java new file mode 100644 index 0000000..71bd82f --- /dev/null +++ b/src/main/java/org/example/visitor/MetricsCounter.java @@ -0,0 +1,54 @@ +package org.example.visitor; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class MetricsCounter { + private static final String OBJECT_CLASS_NAME = "java/lang/Object"; + + public int maxDepth(Map> inheritanceGraph) { + return leafs(inheritanceGraph, OBJECT_CLASS_NAME, 1) + .stream() + .max(Comparator.comparingInt(Leaf::depth)) + .orElse(new Leaf(OBJECT_CLASS_NAME, 1)) + .depth(); + } + + public double averageDepth(Map> inheritanceGraph) { + return leafs(inheritanceGraph, OBJECT_CLASS_NAME, 1) + .stream() + .mapToInt(Leaf::depth) + .average() + .orElse(0.0); + } + + public double overridenMethodsCount(Map> inheritanceGraph, Map classes) { + var sum = 0L; + var n = 0; + for (var parent : inheritanceGraph.keySet()) { + if (classes.get(parent) != null) { + for (var child : inheritanceGraph.get(parent)) { + var parentClassMethods = classes.get(parent).methods(); + var childClassMethods = child.methods(); + sum += childClassMethods.stream().filter(parentClassMethods::contains).count(); + n++; + } + } + } + return (double) sum / n; + } + + private Set leafs(Map> inheritanceGraph, String clazz, int currentDepth) { + var descendants = inheritanceGraph.get(clazz); + if (descendants == null || descendants.isEmpty()) { + return Set.of(new Leaf(clazz, currentDepth)); + } + return descendants + .stream() + .flatMap(d -> leafs(inheritanceGraph, d.name(), currentDepth + 1).stream()) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/java/org/example/visitor/OutputMetrics.java b/src/main/java/org/example/visitor/OutputMetrics.java new file mode 100644 index 0000000..d46acb4 --- /dev/null +++ b/src/main/java/org/example/visitor/OutputMetrics.java @@ -0,0 +1,10 @@ +package org.example.visitor; + +public record OutputMetrics( + int maxDepth, + double averageDepth, + double averageClassFieldsCount, + double averageOverridenMethodsCount, + MethodMetrics abcMetrics, + double abc +) {} diff --git a/src/main/java/org/example/visitor/PrettyPrinter.java b/src/main/java/org/example/visitor/PrettyPrinter.java new file mode 100644 index 0000000..d0a95d5 --- /dev/null +++ b/src/main/java/org/example/visitor/PrettyPrinter.java @@ -0,0 +1,27 @@ +package org.example.visitor; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.*; +import java.util.Arrays; + +public class PrettyPrinter { + private final ObjectMapper objectMapper = new ObjectMapper(); + + public void print(OutputMetrics metrics, OutputStream... outputs) { + Arrays.stream(outputs).forEach((out) -> print(metrics, out)); + } + + public void print(OutputMetrics metrics, OutputStream stream) { + try ( + OutputStreamWriter ow = new OutputStreamWriter(stream); + BufferedWriter bw = new BufferedWriter(ow) + ) { + var jsonMetrics = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(metrics); + bw.write(jsonMetrics); + bw.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +}