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..5f983ae 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,6 +15,13 @@ dependencies { 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.0") + implementation("org.slf4j:slf4j-api:2.0.17") + implementation("ch.qos.logback:logback-classic:1.5.19") + + compileOnly("org.projectlombok:lombok:1.18.30") + annotationProcessor("org.projectlombok:lombok:1.18.30") + testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/org/example/Main.java b/src/main/java/org/example/Main.java new file mode 100644 index 0000000..f4bc2a6 --- /dev/null +++ b/src/main/java/org/example/Main.java @@ -0,0 +1,75 @@ +package org.example; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import org.example.analyzer.JarAnalyzer; +import org.example.analyzer.MetricsCalculator; +import org.example.model.ClassInfo; +import org.example.model.MetricsResult; +import org.example.output.ConsoleMetricsPrinter; +import org.example.output.JsonMetricsPrinter; +import org.example.output.MetricsPrinter; + +public class Main { + public static void main(String[] args) { + String inputPath = null; + String outputPath = null; + + for (int i = 0; i < args.length; i++) { + switch (args[i]) { + case "--input" -> { + if (i + 1 < args.length) { + inputPath = args[++i]; + } + } + case "--output" -> { + if (i + 1 < args.length) { + outputPath = args[++i]; + } + } + default -> throw new IllegalArgumentException("Unknown option: " + args[i]); + } + } + + Path jarPath = getValidJarPath(inputPath); + + try { + analyzeJarInternal(jarPath, outputPath); + } catch (IOException e) { + throw new RuntimeException("Error analyzing jar " + jarPath + ": " + e.getMessage(), e); + } + } + + private static Path getValidJarPath(String inputPath) { + if (inputPath == null) { + throw new IllegalArgumentException("JAR file is not provided"); + } + + Path jarPath = Path.of(inputPath); + if (!Files.exists(jarPath)) { + throw new IllegalArgumentException("JAR file does not exist: " + jarPath); + } + + return jarPath; + } + + private static void analyzeJarInternal(Path jarPath, String outputPath) throws IOException { + JarAnalyzer jarAnalyzer = new JarAnalyzer(); + MetricsCalculator calculator = new MetricsCalculator(); + + Map classes = jarAnalyzer.getJarClassInfo(jarPath); + MetricsResult result = calculator.calculate(classes, jarPath.getFileName().toString()); + + MetricsPrinter consoleMetricsPrinter = new ConsoleMetricsPrinter(); + consoleMetricsPrinter.print(result); + + if (outputPath != null) { + MetricsPrinter jsonMetricsPrinter = new JsonMetricsPrinter(Path.of(outputPath)); + jsonMetricsPrinter.print(result); + } + } +} + diff --git a/src/main/java/org/example/analyzer/JarAnalyzer.java b/src/main/java/org/example/analyzer/JarAnalyzer.java new file mode 100644 index 0000000..9c32b7e --- /dev/null +++ b/src/main/java/org/example/analyzer/JarAnalyzer.java @@ -0,0 +1,60 @@ +package org.example.analyzer; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.example.model.ClassInfo; +import org.example.visitor.ClassInfoCollector; +import org.objectweb.asm.ClassReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JarAnalyzer { + private static final Logger LOGGER = LoggerFactory.getLogger(JarAnalyzer.class); + + private static final String CLASS_EXTENSION = ".class"; + private static final Set META_CLASS_NAMES = Set.of("module-info.class", "package-info.class"); + + public Map getJarClassInfo(Path jarPath) throws IOException { + Map classes = new HashMap<>(); + + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + var entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + + if (!entryName.endsWith(CLASS_EXTENSION)) { + continue; + } + + if (META_CLASS_NAMES.stream().anyMatch(entryName::endsWith)) { + continue; + } + + try (InputStream inputStream = jarFile.getInputStream(entry)) { + ClassInfo classInfo = analyzeClass(inputStream); + classes.put(classInfo.name(), classInfo); + } catch (Exception e) { + LOGGER.error("Error while analyzing class {}: {}", entryName, e.getMessage(), e); + } + } + } + + return classes; + } + + private ClassInfo analyzeClass(InputStream classInputStream) throws IOException { + ClassReader classReader = new ClassReader(classInputStream); + ClassInfoCollector collector = new ClassInfoCollector(); + classReader.accept(collector, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + return collector.getResult(); + } +} + diff --git a/src/main/java/org/example/analyzer/MetricsCalculator.java b/src/main/java/org/example/analyzer/MetricsCalculator.java new file mode 100644 index 0000000..3ff9318 --- /dev/null +++ b/src/main/java/org/example/analyzer/MetricsCalculator.java @@ -0,0 +1,207 @@ +package org.example.analyzer; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.example.model.ClassInfo; +import org.example.model.MethodInfo; +import org.example.model.MetricsResult; + +public class MetricsCalculator { + private static final String OBJECT_CLASS_NAME = "java/lang/Object"; + + public MetricsResult calculate(Map classes, String jarFileName) { + var regularClasses = classes.values().stream() + .filter(c -> !c.isInterface()) + .toList(); + + if (regularClasses.isEmpty()) { + return MetricsResult.builder() + .jarFileName(jarFileName) + .build(); + } + + Map depthCache = new HashMap<>(); + int maxDepth = 0; + int totalDepth = 0; + + for (ClassInfo classInfo : regularClasses) { + int depth = calculateDepth(classInfo.name(), classes, depthCache); + maxDepth = Math.max(maxDepth, depth); + totalDepth += depth; + } + double avgDepth = (double) totalDepth / regularClasses.size(); + + long totalAssignments = 0; + long totalBranches = 0; + long totalConditions = 0; + int totalMethods = 0; + int totalFields = 0; + + for (ClassInfo classInfo : classes.values()) { + for (MethodInfo method : classInfo.methods()) { + totalAssignments += method.assignments(); + totalBranches += method.branches(); + totalConditions += method.conditions(); + totalMethods++; + } + totalFields += classInfo.fieldCount(); + } + + double abcMetric = Math.sqrt( + (double) totalAssignments * totalAssignments + + (double) totalBranches * totalBranches + + (double) totalConditions * totalConditions + ); + + double avgFields = (double) totalFields / classes.size(); + + Map> parentMethodsCache = new HashMap<>(); + int totalOverridden = 0; + for (ClassInfo classInfo : regularClasses) { + totalOverridden += countOverriddenMethods(classInfo, classes, parentMethodsCache); + } + double avgOverridden = (double) totalOverridden / regularClasses.size(); + + return MetricsResult.builder() + .jarFileName(jarFileName) + .totalClasses(classes.size()) + .totalMethods(totalMethods) + .totalFields(totalFields) + .maxInheritanceDepth(maxDepth) + .avgInheritanceDepth(avgDepth) + .totalAssignments(totalAssignments) + .totalBranches(totalBranches) + .totalConditions(totalConditions) + .abcMetric(abcMetric) + .avgOverriddenMethods(avgOverridden) + .avgFieldsPerClass(avgFields) + .build(); + } + + private int calculateDepth( + String className, + Map classes, + Map cache + ) { + if (className == null || OBJECT_CLASS_NAME.equals(className)) { + return 0; + } + + if (cache.containsKey(className)) { + return cache.get(className); + } + + ClassInfo classInfo = classes.get(className); + + int depth; + if (classInfo != null) { + depth = 1 + calculateDepth(classInfo.superName(), classes, cache); + } else { + depth = 1; + } + + cache.put(className, depth); + return depth; + } + + private int countOverriddenMethods( + ClassInfo classInfo, + Map classes, + Map> cache + ) { + Set parentMethods = new HashSet<>( + collectSuperClassMethods(classInfo.superName(), classes, cache) + ); + + for (String iface : classInfo.interfaces()) { + parentMethods.addAll(collectInterfaceMethods(iface, classes, cache)); + } + + int overridden = 0; + for (MethodInfo method : classInfo.methods()) { + if (!method.canBeOverride()) { + continue; + } + + String signature = method.name() + method.descriptor(); + if (parentMethods.contains(signature)) { + overridden++; + } + } + + return overridden; + } + + private Set collectSuperClassMethods( + String superName, + Map classes, + Map> cache + ) { + if (superName == null || OBJECT_CLASS_NAME.equals(superName)) { + return Collections.emptySet(); + } + + if (cache.containsKey(superName)) { + return cache.get(superName); + } + + ClassInfo parent = classes.get(superName); + if (parent == null) { + return Collections.emptySet(); + } + + Set methods = new HashSet<>(collectSuperClassMethods(parent.superName(), classes, cache)); + + for (String iface : parent.interfaces()) { + methods.addAll(collectInterfaceMethods(iface, classes, cache)); + } + + for (MethodInfo method : parent.methods()) { + if (method.isOverrideable()) { + methods.add(method.name() + method.descriptor()); + } + } + + cache.put(superName, methods); + return methods; + } + + private Set collectInterfaceMethods( + String ifaceName, + Map classes, + Map> cache + ) { + if (ifaceName == null) { + return Collections.emptySet(); + } + + if (cache.containsKey(ifaceName)) { + return cache.get(ifaceName); + } + + ClassInfo iface = classes.get(ifaceName); + if (iface == null) { + return Collections.emptySet(); + } + + Set methods = new HashSet<>(); + + for (String parentIface : iface.interfaces()) { + methods.addAll(collectInterfaceMethods(parentIface, classes, cache)); + } + + for (MethodInfo method : iface.methods()) { + if (!method.isConstructor() && !method.isStaticInitializer()) { + methods.add(method.name() + method.descriptor()); + } + } + + cache.put(ifaceName, methods); + return methods; + } +} + diff --git a/src/main/java/org/example/model/ClassInfo.java b/src/main/java/org/example/model/ClassInfo.java new file mode 100644 index 0000000..6a74224 --- /dev/null +++ b/src/main/java/org/example/model/ClassInfo.java @@ -0,0 +1,19 @@ +package org.example.model; + +import java.util.List; + +import static org.objectweb.asm.Opcodes.ACC_INTERFACE; + +public record ClassInfo( + String name, + String superName, + List interfaces, + int accessFlags, + int fieldCount, + List methods +) { + public boolean isInterface() { + return (accessFlags & ACC_INTERFACE) != 0; + } +} + diff --git a/src/main/java/org/example/model/MethodInfo.java b/src/main/java/org/example/model/MethodInfo.java new file mode 100644 index 0000000..25738f1 --- /dev/null +++ b/src/main/java/org/example/model/MethodInfo.java @@ -0,0 +1,52 @@ +package org.example.model; + +import static org.objectweb.asm.Opcodes.ACC_BRIDGE; +import static org.objectweb.asm.Opcodes.ACC_FINAL; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_STATIC; + +public record MethodInfo( + String name, + String descriptor, + int accessFlags, + int assignments, + int branches, + int conditions +) { + public boolean isConstructor() { + return "".equals(name); + } + + public boolean isStaticInitializer() { + return "".equals(name); + } + + public boolean isStatic() { + return hasFlag(ACC_STATIC); + } + + public boolean isPrivate() { + return hasFlag(ACC_PRIVATE); + } + + public boolean isFinal() { + return hasFlag(ACC_FINAL); + } + + public boolean isBridge() { + return hasFlag(ACC_BRIDGE); + } + + public boolean isOverrideable() { + return !isConstructor() && !isStaticInitializer() && !isStatic() && !isPrivate() && !isFinal(); + } + + public boolean canBeOverride() { + return !isConstructor() && !isStaticInitializer() && !isStatic() && !isPrivate() && !isBridge(); + } + + private boolean hasFlag(int flag) { + return (accessFlags & flag) != 0; + } +} + diff --git a/src/main/java/org/example/model/MetricsResult.java b/src/main/java/org/example/model/MetricsResult.java new file mode 100644 index 0000000..4f76b50 --- /dev/null +++ b/src/main/java/org/example/model/MetricsResult.java @@ -0,0 +1,19 @@ +package org.example.model; + +import lombok.Builder; + +@Builder +public record MetricsResult( + String jarFileName, + int totalClasses, + int totalMethods, + int totalFields, + int maxInheritanceDepth, + double avgInheritanceDepth, + long totalAssignments, + long totalBranches, + long totalConditions, + double abcMetric, + double avgOverriddenMethods, + double avgFieldsPerClass +) {} diff --git a/src/main/java/org/example/output/ConsoleMetricsPrinter.java b/src/main/java/org/example/output/ConsoleMetricsPrinter.java new file mode 100644 index 0000000..316e59f --- /dev/null +++ b/src/main/java/org/example/output/ConsoleMetricsPrinter.java @@ -0,0 +1,30 @@ +package org.example.output; + +import org.example.model.MetricsResult; + +public class ConsoleMetricsPrinter implements MetricsPrinter { + + @Override + public void print(MetricsResult result) { + System.out.println(); + System.out.println("=== Metrics Report: " + result.jarFileName() + " ==="); + System.out.println(); + System.out.println("Total classes: " + result.totalClasses()); + System.out.println("Total methods: " + result.totalMethods()); + System.out.println("Total fields: " + result.totalFields()); + System.out.println(); + System.out.println("Inheritance depth:"); + System.out.println(" Max: " + result.maxInheritanceDepth()); + System.out.printf(" Avg: %.2f%n", result.avgInheritanceDepth()); + System.out.println(); + System.out.println("ABC metrics:"); + System.out.println(" Assignments: " + result.totalAssignments()); + System.out.println(" Branches: " + result.totalBranches()); + System.out.println(" Conditions: " + result.totalConditions()); + System.out.printf(" ABC metric: %.2f%n", result.abcMetric()); + System.out.println(); + System.out.printf("Avg overridden methods: %.2f%n", result.avgOverriddenMethods()); + System.out.printf("Avg fields per class: %.2f%n", result.avgFieldsPerClass()); + System.out.println(); + } +} diff --git a/src/main/java/org/example/output/JsonMetricsPrinter.java b/src/main/java/org/example/output/JsonMetricsPrinter.java new file mode 100644 index 0000000..3898541 --- /dev/null +++ b/src/main/java/org/example/output/JsonMetricsPrinter.java @@ -0,0 +1,32 @@ +package org.example.output; + +import java.io.IOException; +import java.nio.file.Path; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.example.model.MetricsResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JsonMetricsPrinter implements MetricsPrinter { + + private static final Logger LOGGER = LoggerFactory.getLogger(JsonMetricsPrinter.class); + private static final ObjectMapper MAPPER = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT); + + private final Path outputPath; + + public JsonMetricsPrinter(Path outputPath) { + this.outputPath = outputPath; + } + + @Override + public void print(MetricsResult result) { + try { + MAPPER.writerWithDefaultPrettyPrinter().writeValue(outputPath.toFile(), result); + } catch (IOException e) { + LOGGER.error("Failed to write json metrics: {}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/org/example/output/MetricsPrinter.java b/src/main/java/org/example/output/MetricsPrinter.java new file mode 100644 index 0000000..9c097f6 --- /dev/null +++ b/src/main/java/org/example/output/MetricsPrinter.java @@ -0,0 +1,7 @@ +package org.example.output; + +import org.example.model.MetricsResult; + +public interface MetricsPrinter { + void print(MetricsResult result); +} diff --git a/src/main/java/org/example/visitor/ABCMethodVisitor.java b/src/main/java/org/example/visitor/ABCMethodVisitor.java new file mode 100644 index 0000000..f0c0c9f --- /dev/null +++ b/src/main/java/org/example/visitor/ABCMethodVisitor.java @@ -0,0 +1,85 @@ +package org.example.visitor; + +import java.util.function.Consumer; + +import org.example.model.MethodInfo; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; + +import static org.objectweb.asm.Opcodes.ASTORE; +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.ISTORE; +import static org.objectweb.asm.Opcodes.JSR; + +public class ABCMethodVisitor extends MethodVisitor { + + private final String name; + private final String descriptor; + private final int access; + private final Consumer onComplete; + private int assignments = 0; + private int branches = 0; + private int conditions = 0; + + public ABCMethodVisitor(int apiVersion, String name, String descriptor, int access, + Consumer onComplete) { + super(apiVersion); + this.name = name; + this.descriptor = descriptor; + this.access = access; + this.onComplete = onComplete; + } + + @Override + public void visitVarInsn(int opcode, int varIndex) { + if (opcode >= ISTORE && opcode <= ASTORE) { + assignments++; + } + super.visitVarInsn(opcode, varIndex); + } + + @Override + public void visitIincInsn(int varIndex, int increment) { + assignments++; + super.visitIincInsn(varIndex, increment); + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + branches++; + + if (opcode != GOTO && opcode != JSR) { + conditions++; + } + super.visitJumpInsn(opcode, label); + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { + branches++; + conditions += labels.length; + super.visitTableSwitchInsn(min, max, dflt, labels); + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + branches++; + conditions += labels.length; + super.visitLookupSwitchInsn(dflt, keys, labels); + } + + @Override + public void visitEnd() { + MethodInfo result = new MethodInfo( + name, + descriptor, + access, + assignments, + branches, + conditions + ); + onComplete.accept(result); + super.visitEnd(); + } +} + diff --git a/src/main/java/org/example/visitor/ClassInfoCollector.java b/src/main/java/org/example/visitor/ClassInfoCollector.java new file mode 100644 index 0000000..b3d6383 --- /dev/null +++ b/src/main/java/org/example/visitor/ClassInfoCollector.java @@ -0,0 +1,63 @@ +package org.example.visitor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.example.model.ClassInfo; +import org.example.model.MethodInfo; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; + +import static org.objectweb.asm.Opcodes.ASM9; + +public class ClassInfoCollector extends ClassVisitor { + + private String name; + private String superName; + private List interfaces; + private int access; + private int fieldCount = 0; + private final List methods = new ArrayList<>(); + + public ClassInfoCollector() { + super(ASM9); + } + + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + this.name = name; + this.superName = superName; + this.access = access; + this.interfaces = interfaces != null ? Arrays.asList(interfaces) : Collections.emptyList(); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, + String signature, Object value) { + fieldCount++; + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + return new ABCMethodVisitor(api, name, descriptor, access, methods::add); + } + + public ClassInfo getResult() { + return new ClassInfo( + name, + superName, + interfaces, + access, + fieldCount, + List.copyOf(methods) + ); + } +} + diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..30542af --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + + %d{dd.MM.yyyy HH:mm:ss.SSS} [%thread] %-5level %logger{20} - %msg%n + + + + + + +