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..f8a3e78 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,6 +18,8 @@ dependencies { testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0") } tasks.test { diff --git a/output.json b/output.json new file mode 100644 index 0000000..22783fb --- /dev/null +++ b/output.json @@ -0,0 +1,8 @@ +{ + "classesAnalyzed" : 330, + "abc" : 2577.6365143285816, + "maxInheritanceDepth" : 5, + "avgInheritanceDepth" : 1.4393939393939394, + "avgOverriddenMethods" : 2.2790697674418605, + "avgFieldsPerClass" : 2.609090909090909 +} \ No newline at end of file diff --git a/src/main/java/analyzer/ClassInfo.java b/src/main/java/analyzer/ClassInfo.java new file mode 100644 index 0000000..f2a39fd --- /dev/null +++ b/src/main/java/analyzer/ClassInfo.java @@ -0,0 +1,20 @@ +package analyzer; + +import java.util.HashSet; +import java.util.Set; + +public class ClassInfo { + public final String name; + public final String superName; + public final Set methods = new HashSet<>(); + + public int fieldsCount = 0; + public int assignmentCount = 0; + public int branchCount = 0; + public int conditionCount = 0; + + public ClassInfo(String name, String superName) { + this.name = name; + this.superName = superName; + } +} diff --git a/src/main/java/analyzer/JarAnalyzer.java b/src/main/java/analyzer/JarAnalyzer.java new file mode 100644 index 0000000..562a28e --- /dev/null +++ b/src/main/java/analyzer/JarAnalyzer.java @@ -0,0 +1,106 @@ +package analyzer; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.objectweb.asm.ClassReader; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import metrics.AbcCalculator; +import metrics.FieldMetricsCalculator; +import metrics.InheritanceDepthCalculator; +import metrics.InheritanceStats; +import metrics.OverrideMethodsCalculator; +import output.MetricsResult; +import visitor.MetricsClassVisitor; + +public class JarAnalyzer { + + public static void main(String[] args) throws IOException { + + if (args.length < 2) { + System.err.println("Expected 2 arguments: "); + System.exit(1); + } + + String jarPath = args[0]; + String jsonOutputPath = args[1]; + + Map classes = new HashMap<>(); + + try (JarFile jarFile = new JarFile(jarPath)) { + Enumeration entries = jarFile.entries(); + + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + + if (!entry.getName().endsWith(".class")) { + continue; + } + + try (InputStream is = jarFile.getInputStream(entry)) { + ClassReader reader = new ClassReader(is); + MetricsClassVisitor visitor = new MetricsClassVisitor(); + reader.accept(visitor, ClassReader.SKIP_DEBUG); + + ClassInfo info = visitor.getClassInfo(); + classes.put(info.name, info); + } + } + } + + + InheritanceStats depthStats = + InheritanceDepthCalculator.calculate(classes); + + double abc = + AbcCalculator.totalAssignments(classes); + + double avgOverriddenMethods = + OverrideMethodsCalculator.averageOverriddenMethods(classes); + + double avgFields = + FieldMetricsCalculator.averageFields(classes); + + MetricsResult result = new MetricsResult( + classes.size(), + abc, + depthStats, + avgOverriddenMethods, + avgFields + ); + + printToConsole(result); + + writeJson(result, jsonOutputPath); + } + + private static void printToConsole(MetricsResult r) { + System.out.println("====== Metrics ======"); + System.out.println("Classes analyzed: " + r.classesAnalyzed); + System.out.println("ABC (assignments): " + r.abc); + System.out.println("Max inheritance depth: " + r.maxInheritanceDepth); + System.out.println("Avg inheritance depth: " + r.avgInheritanceDepth); + System.out.println("Avg overridden methods: " + r.avgOverriddenMethods); + System.out.println("Avg fields per class: " + r.avgFieldsPerClass); + } + + private static void writeJson( + MetricsResult result, + String outputPath + ) throws IOException { + + ObjectMapper mapper = new ObjectMapper(); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + + mapper.writeValue(new File(outputPath), result); + } +} diff --git a/src/main/java/analyzer/MethodSignature.java b/src/main/java/analyzer/MethodSignature.java new file mode 100644 index 0000000..0f0dd88 --- /dev/null +++ b/src/main/java/analyzer/MethodSignature.java @@ -0,0 +1,18 @@ +package analyzer; + +public record MethodSignature(String name, String descriptor) { + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MethodSignature)) { + return false; + } + MethodSignature that = (MethodSignature) o; + return name.equals(that.name) + && descriptor.equals(that.descriptor); + } + +} diff --git a/src/main/java/metrics/AbcCalculator.java b/src/main/java/metrics/AbcCalculator.java new file mode 100644 index 0000000..1cb5c96 --- /dev/null +++ b/src/main/java/metrics/AbcCalculator.java @@ -0,0 +1,16 @@ +package metrics; + +import java.util.Map; + +import analyzer.ClassInfo; + +public class AbcCalculator { + + public static double totalAssignments(Map classes) { + int totalA = classes.values().stream().mapToInt(c -> c.assignmentCount).sum(); + int totalB = classes.values().stream().mapToInt(c -> c.branchCount).sum(); + int totalC = classes.values().stream().mapToInt(c -> c.conditionCount).sum(); + + return Math.sqrt(totalA*totalA + totalB*totalB + totalC*totalC); + } +} diff --git a/src/main/java/metrics/FieldMetricsCalculator.java b/src/main/java/metrics/FieldMetricsCalculator.java new file mode 100644 index 0000000..8d0eb83 --- /dev/null +++ b/src/main/java/metrics/FieldMetricsCalculator.java @@ -0,0 +1,20 @@ +package metrics; + +import java.util.Map; + +import analyzer.ClassInfo; + +public class FieldMetricsCalculator { + + public static double averageFields(Map classes) { + if (classes.isEmpty()) return 0.0; + + int sum = 0; + + for (ClassInfo cls : classes.values()) { + sum += cls.fieldsCount; + } + + return (double) sum / classes.size(); + } +} diff --git a/src/main/java/metrics/InheritanceDepthCalculator.java b/src/main/java/metrics/InheritanceDepthCalculator.java new file mode 100644 index 0000000..b973ded --- /dev/null +++ b/src/main/java/metrics/InheritanceDepthCalculator.java @@ -0,0 +1,42 @@ +package metrics; + +import java.util.Map; + +import analyzer.ClassInfo; + +public class InheritanceDepthCalculator { + + public static InheritanceStats calculate(Map classes) { + if (classes.isEmpty()) { + return new InheritanceStats(0, 0.0); + } + + int max = 0; + int sum = 0; + + for (ClassInfo cls : classes.values()) { + int depth = calculateDepth(cls, classes); + sum += depth; + max = Math.max(max, depth); + } + + double avg = (double) sum / classes.size(); + + return new InheritanceStats(max, avg); + } + + private static int calculateDepth( + ClassInfo cls, + Map classes + ) { + int depth = 1; + String current = cls.superName; + + while (current != null && classes.containsKey(current)) { + depth++; + current = classes.get(current).superName; + } + + return depth; + } +} diff --git a/src/main/java/metrics/InheritanceStats.java b/src/main/java/metrics/InheritanceStats.java new file mode 100644 index 0000000..faf7fad --- /dev/null +++ b/src/main/java/metrics/InheritanceStats.java @@ -0,0 +1,4 @@ +package metrics; + +public record InheritanceStats(int maxDepth, double avgDepth) { +} diff --git a/src/main/java/metrics/OverrideMethodsCalculator.java b/src/main/java/metrics/OverrideMethodsCalculator.java new file mode 100644 index 0000000..3e8cf14 --- /dev/null +++ b/src/main/java/metrics/OverrideMethodsCalculator.java @@ -0,0 +1,62 @@ +package metrics; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import analyzer.ClassInfo; +import analyzer.MethodSignature; + +public class OverrideMethodsCalculator { + + public static double averageOverriddenMethods(Map classes) { + int totalOverrides = 0; + int classCount = 0; + + for (ClassInfo cls : classes.values()) { + if (cls.superName == null) continue; + + ClassInfo superCls = classes.get(cls.superName); + if (superCls == null) continue; + + int overridden = countOverrides(cls, classes); + totalOverrides += overridden; + classCount = overridden > 0 ? classCount + 1 : classCount; + } + + if (classCount == 0) return 0.0; + + return (double) totalOverrides / classCount; + } + + public static int countOverrides(ClassInfo cls, Map classes) { + int count = 0; + Set seen = new HashSet<>(); + + String superName = cls.superName; + + while (superName != null && classes.containsKey(superName)) { + ClassInfo superCls = classes.get(superName); + + for (MethodSignature m : cls.methods) { + if (!seen.contains(m) && (isObjectMethod(m) || superCls.methods.contains(m))) { + count++; + seen.add(m); + } + } + + superName = superCls.superName; + } + + return count; + } + + private static boolean isObjectMethod(MethodSignature method) { + String methodSignature = method.name() + method.descriptor(); + return methodSignature.equals("toString()Ljava/lang/String;") || + methodSignature.equals("equals(Ljava/lang/Object;)Z") || + methodSignature.equals("hashCode()I") || + methodSignature.equals("clone()Ljava/lang/Object;") || + methodSignature.equals("finalize()V"); + } +} diff --git a/src/main/java/output/MetricsResult.java b/src/main/java/output/MetricsResult.java new file mode 100644 index 0000000..9d081a1 --- /dev/null +++ b/src/main/java/output/MetricsResult.java @@ -0,0 +1,30 @@ +package output; + +import metrics.InheritanceStats; + +public class MetricsResult { + + public int classesAnalyzed; + public double abc; + + public int maxInheritanceDepth; + public double avgInheritanceDepth; + + public double avgOverriddenMethods; + public double avgFieldsPerClass; + + public MetricsResult( + int classesAnalyzed, + double abc, + InheritanceStats inheritanceStats, + double avgOverriddenMethods, + double avgFieldsPerClass + ) { + this.classesAnalyzed = classesAnalyzed; + this.abc = abc; + this.maxInheritanceDepth = inheritanceStats.maxDepth(); + this.avgInheritanceDepth = inheritanceStats.avgDepth(); + this.avgOverriddenMethods = avgOverriddenMethods; + this.avgFieldsPerClass = avgFieldsPerClass; + } +} diff --git a/src/main/java/visitor/AbcMethodVisitor.java b/src/main/java/visitor/AbcMethodVisitor.java new file mode 100644 index 0000000..18a8424 --- /dev/null +++ b/src/main/java/visitor/AbcMethodVisitor.java @@ -0,0 +1,62 @@ +package visitor; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import analyzer.ClassInfo; + +public class AbcMethodVisitor extends MethodVisitor { + + private final ClassInfo classInfo; + + private static final Set CONDITIONAL_JUMPS = new HashSet<>(Arrays.asList( + Opcodes.IFEQ, Opcodes.IFNE, Opcodes.IFLT, Opcodes.IFGE, Opcodes.IFGT, Opcodes.IFLE, + Opcodes.IF_ICMPEQ, Opcodes.IF_ICMPNE, Opcodes.IF_ICMPLT, Opcodes.IF_ICMPGE, + Opcodes.IF_ICMPGT, Opcodes.IF_ICMPLE, Opcodes.IF_ACMPEQ, Opcodes.IF_ACMPNE, + Opcodes.IFNULL, Opcodes.IFNONNULL + )); + + + public AbcMethodVisitor(MethodVisitor mv, ClassInfo classInfo) { + super(Opcodes.ASM9, mv); + this.classInfo = classInfo; + } + + @Override + public void visitVarInsn(int opcode, int var) { + if (opcode >= Opcodes.ISTORE && opcode <= Opcodes.ASTORE) { + classInfo.assignmentCount++; + } + super.visitVarInsn(opcode, var); + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + classInfo.branchCount++; + + if (CONDITIONAL_JUMPS.contains(opcode)) { + classInfo.conditionCount++; + } + + super.visitJumpInsn(opcode, label); + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + classInfo.branchCount++; + classInfo.conditionCount += labels.length + 1; + super.visitLookupSwitchInsn(dflt, keys, labels); + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { + classInfo.branchCount++; + classInfo.conditionCount += labels.length + 1; + super.visitTableSwitchInsn(min, max, dflt, labels); + } +} diff --git a/src/main/java/visitor/MetricsClassVisitor.java b/src/main/java/visitor/MetricsClassVisitor.java new file mode 100644 index 0000000..6530636 --- /dev/null +++ b/src/main/java/visitor/MetricsClassVisitor.java @@ -0,0 +1,77 @@ +package visitor; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import analyzer.ClassInfo; +import analyzer.MethodSignature; + +public class MetricsClassVisitor extends ClassVisitor { + + private ClassInfo classInfo; + + public MetricsClassVisitor() { + super(Opcodes.ASM9); + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces + ) { + this.classInfo = new ClassInfo(name, superName); + } + + @Override + public FieldVisitor visitField( + int access, + String name, + String descriptor, + String signature, + Object value + ) { + classInfo.fieldsCount++; + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod( + int access, + String name, + String descriptor, + String signature, + String[] exceptions + ) { + if (isEligibleForOverride(access, name)) { + classInfo.methods.add(new MethodSignature(name, descriptor)); + } + + return new AbcMethodVisitor( + super.visitMethod(access, name, descriptor, signature, exceptions), + classInfo + ); + } + + private boolean isEligibleForOverride(int access, String name) { + if (name.equals("") || name.equals("")) { + return false; + } + if ((access & Opcodes.ACC_PRIVATE) != 0) { + return false; + } + if ((access & Opcodes.ACC_STATIC) != 0) { + return false; + } + return true; + } + + public ClassInfo getClassInfo() { + return classInfo; + } +}