From b518fa7db42a66a0215b880ae9eb396eca44be8c Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:11:31 +0000 Subject: [PATCH 1/6] add deadline --- README.md | 1 + 1 file changed, 1 insertion(+) 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 файла From a6704d1b71a52a1713e403458812165158e784b1 Mon Sep 17 00:00:00 2001 From: Gogolev Andrey Date: Wed, 3 Dec 2025 23:52:04 +0300 Subject: [PATCH 2/6] add visitors for classes and methods --- build.gradle.kts | 10 +- gradlew | 0 .../java/org/example/model/ABCMetrics.java | 66 +++++++++ .../java/org/example/model/ClassInfo.java | 130 ++++++++++++++++++ .../org/example/model/JarAnalysisResult.java | 34 +++++ .../java/org/example/model/MethodInfo.java | 24 ++++ .../org/example/service/JarProcessor.java | 56 ++++++++ .../org/example/service/ReportGenerator.java | 70 ++++++++++ .../org/example/util/ByteCodePrinter.java | 2 +- .../visitor/ABCMetricsMethodVisitor.java | 90 ++++++++++++ .../org/example/visitor/ClassInfoVisitor.java | 84 +++++++++++ 11 files changed, 564 insertions(+), 2 deletions(-) mode change 100644 => 100755 gradlew create mode 100644 src/main/java/org/example/model/ABCMetrics.java create mode 100644 src/main/java/org/example/model/ClassInfo.java create mode 100644 src/main/java/org/example/model/JarAnalysisResult.java create mode 100644 src/main/java/org/example/model/MethodInfo.java create mode 100644 src/main/java/org/example/service/JarProcessor.java create mode 100644 src/main/java/org/example/service/ReportGenerator.java create mode 100644 src/main/java/org/example/visitor/ABCMetricsMethodVisitor.java create mode 100644 src/main/java/org/example/visitor/ClassInfoVisitor.java diff --git a/build.gradle.kts b/build.gradle.kts index 2b92afd..a9065d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,6 +15,8 @@ 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.15.3") + testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") @@ -22,4 +24,10 @@ dependencies { tasks.test { useJUnitPlatform() -} \ No newline at end of file +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } +} diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/org/example/model/ABCMetrics.java b/src/main/java/org/example/model/ABCMetrics.java new file mode 100644 index 0000000..a9eecc2 --- /dev/null +++ b/src/main/java/org/example/model/ABCMetrics.java @@ -0,0 +1,66 @@ +package org.example.model; + +public final class ABCMetrics { + private int assignments; + private int branches; + private int conditions; + + public ABCMetrics() { + this(0, 0, 0); + } + + public ABCMetrics(int assignments, int branches, int conditions) { + this.assignments = assignments; + this.branches = branches; + this.conditions = conditions; + } + + public void incrementAssignments() { + assignments++; + } + + public void incrementBranches() { + branches++; + } + + public void incrementConditions() { + conditions++; + } + + public void incrementConditions(int delta) { + conditions += delta; + } + + public void add(ABCMetrics other) { + this.assignments += other.assignments; + this.branches += other.branches; + this.conditions += other.conditions; + } + + public int getAssignments() { + return assignments; + } + + public int getBranches() { + return branches; + } + + public int getConditions() { + return conditions; + } + + public double calculateMagnitude() { + return Math.sqrt( + (double) assignments * assignments + + (double) branches * branches + + (double) conditions * conditions + ); + } + + @Override + public String toString() { + return String.format("ABC(A=%d, B=%d, C=%d, magnitude=%.2f)", + assignments, branches, conditions, calculateMagnitude()); + } +} + 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..13acef1 --- /dev/null +++ b/src/main/java/org/example/model/ClassInfo.java @@ -0,0 +1,130 @@ +package org.example.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public final class ClassInfo { + private final String name; + private final String superName; + private final List interfaces; + private final Set methods; + private final int fieldCount; + private final ABCMetrics abcMetrics; + private final boolean isInterface; + + private ClassInfo(Builder builder) { + this.name = Objects.requireNonNull(builder.name, "name cannot be null"); + this.superName = builder.superName; + this.interfaces = List.copyOf(builder.interfaces); + this.methods = Set.copyOf(builder.methods); + this.fieldCount = builder.fieldCount; + this.abcMetrics = builder.abcMetrics; + this.isInterface = builder.isInterface; + } + + public String getName() { + return name; + } + + public String getSuperName() { + return superName; + } + + public List getInterfaces() { + return interfaces; + } + + public Set getMethods() { + return methods; + } + + public int getFieldCount() { + return fieldCount; + } + + public ABCMetrics getAbcMetrics() { + return abcMetrics; + } + + public boolean isInterface() { + return isInterface; + } + + @Override + public String toString() { + return String.format("ClassInfo{name='%s', super='%s', interfaces=%s, methods=%d, fields=%d}", + name, superName, interfaces, methods.size(), fieldCount); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String name; + private String superName; + private final List interfaces = new ArrayList<>(); + private final Set methods = new HashSet<>(); + private int fieldCount = 0; + private ABCMetrics abcMetrics = new ABCMetrics(); + private boolean isInterface = false; + + private Builder() { + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder superName(String superName) { + this.superName = superName; + return this; + } + + public Builder addInterface(String interfaceName) { + if (interfaceName != null) { + this.interfaces.add(interfaceName); + } + return this; + } + + public Builder addInterfaces(String[] interfaceNames) { + if (interfaceNames != null) { + for (String name : interfaceNames) { + addInterface(name); + } + } + return this; + } + + public Builder addMethod(MethodInfo method) { + this.methods.add(method); + return this; + } + + public Builder incrementFieldCount() { + this.fieldCount++; + return this; + } + + public Builder abcMetrics(ABCMetrics abcMetrics) { + this.abcMetrics = abcMetrics; + return this; + } + + public Builder isInterface(boolean isInterface) { + this.isInterface = isInterface; + return this; + } + + public ClassInfo build() { + return new ClassInfo(this); + } + } +} + diff --git a/src/main/java/org/example/model/JarAnalysisResult.java b/src/main/java/org/example/model/JarAnalysisResult.java new file mode 100644 index 0000000..2b683ac --- /dev/null +++ b/src/main/java/org/example/model/JarAnalysisResult.java @@ -0,0 +1,34 @@ +package org.example.model; + +public record JarAnalysisResult( + String jarFileName, + int totalClasses, + int totalInterfaces, + InheritanceMetrics inheritance, + ABCSummary abc, + double averageOverriddenMethods, + double averageFieldsPerClass +) { + public record InheritanceMetrics( + int maxDepth, + double averageDepth + ) { + } + + public record ABCSummary( + int totalAssignments, + int totalBranches, + int totalConditions, + double magnitude + ) { + public static ABCSummary from(ABCMetrics metrics) { + return new ABCSummary( + metrics.getAssignments(), + metrics.getBranches(), + metrics.getConditions(), + metrics.calculateMagnitude() + ); + } + } +} + 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..d6c24e8 --- /dev/null +++ b/src/main/java/org/example/model/MethodInfo.java @@ -0,0 +1,24 @@ +package org.example.model; + +import java.util.Objects; + +public record MethodInfo(String name, String descriptor) { + public MethodInfo { + Objects.requireNonNull(name, "name cannot be null"); + Objects.requireNonNull(descriptor, "descriptor cannot be null"); + } + + public boolean isConstructor() { + return "".equals(name); + } + + public boolean isStaticInitializer() { + return "".equals(name); + } + + @Override + public String toString() { + return name + descriptor; + } +} + diff --git a/src/main/java/org/example/service/JarProcessor.java b/src/main/java/org/example/service/JarProcessor.java new file mode 100644 index 0000000..8ac8ea3 --- /dev/null +++ b/src/main/java/org/example/service/JarProcessor.java @@ -0,0 +1,56 @@ +package org.example.service; + +import org.example.model.ClassInfo; +import org.example.visitor.ClassInfoVisitor; +import org.objectweb.asm.ClassReader; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class JarProcessor { + private static final Logger log = Logger.getLogger(JarProcessor.class.getName()); + + public List process(Path jarPath) throws IOException { + List classes = new ArrayList<>(); + + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (isClassFile(entry)) { + classes.add(processClassEntry(jarFile, entry)); + } + } + } + + log.info(() -> String.format("Processed %d classes from %s", classes.size(), jarPath.getFileName())); + return classes; + } + + private boolean isClassFile(JarEntry entry) { + return !entry.isDirectory() && entry.getName().endsWith(".class"); + } + + private ClassInfo processClassEntry(JarFile jarFile, JarEntry entry) { + try (InputStream inputStream = jarFile.getInputStream(entry)) { + ClassReader classReader = new ClassReader(inputStream); + ClassInfoVisitor collector = new ClassInfoVisitor(); + + classReader.accept(collector, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + + return collector.getClassInfo(); + } catch (Exception e) { + log.log(Level.WARNING, "Failed to process class: " + entry.getName(), e); + } + } +} + diff --git a/src/main/java/org/example/service/ReportGenerator.java b/src/main/java/org/example/service/ReportGenerator.java new file mode 100644 index 0000000..6aa7230 --- /dev/null +++ b/src/main/java/org/example/service/ReportGenerator.java @@ -0,0 +1,70 @@ +package org.example.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.example.model.JarAnalysisResult; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ReportGenerator { + private static final String SEPARATOR = "═".repeat(60); + private static final String THIN_SEPARATOR = "─".repeat(60); + + private final ObjectMapper objectMapper; + + public ReportGenerator() { + this.objectMapper = new ObjectMapper(); + this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + } + + public void printToConsole(JarAnalysisResult result) { + printToConsole(result, System.out); + } + + public void printToConsole(JarAnalysisResult result, PrintStream out) { + out.println(); + out.println(SEPARATOR); + out.println(" JAR BYTECODE ANALYSIS REPORT"); + out.println(SEPARATOR); + out.println(); + + out.printf(" File: %s%n", result.jarFileName()); + out.printf(" Total classes: %d%n", result.totalClasses()); + out.printf(" Total interfaces: %d%n", result.totalInterfaces()); + out.println(); + + out.println(THIN_SEPARATOR); + out.println(" INHERITANCE METRICS"); + out.println(THIN_SEPARATOR); + out.printf(" Maximum inheritance depth: %d%n", result.inheritance().maxDepth()); + out.printf(" Average inheritance depth: %.2f%n", result.inheritance().averageDepth()); + out.println(); + + out.println(THIN_SEPARATOR); + out.println(" ABC METRICS"); + out.println(THIN_SEPARATOR); + out.printf(" Assignments (A): %d%n", result.abc().totalAssignments()); + out.printf(" Branches (B): %d%n", result.abc().totalBranches()); + out.printf(" Conditions/Calls (C): %d%n", result.abc().totalConditions()); + out.printf(" ABC Magnitude: %.2f%n", result.abc().magnitude()); + out.println(); + + out.println(THIN_SEPARATOR); + out.println(" CLASS STRUCTURE METRICS"); + out.println(THIN_SEPARATOR); + out.printf(" Average overridden methods per class: %.2f%n", result.averageOverriddenMethods()); + out.printf(" Average fields per class: %.2f%n", result.averageFieldsPerClass()); + out.println(); + + out.println(SEPARATOR); + } + + public void writeToJson(JarAnalysisResult result, Path outputPath) throws IOException { + String json = objectMapper.writeValueAsString(result); + Files.writeString(outputPath, json); + } +} + diff --git a/src/main/java/org/example/util/ByteCodePrinter.java b/src/main/java/org/example/util/ByteCodePrinter.java index 3d1ca38..9f906ed 100644 --- a/src/main/java/org/example/util/ByteCodePrinter.java +++ b/src/main/java/org/example/util/ByteCodePrinter.java @@ -82,7 +82,7 @@ public void printBytecode(ClassNode cn) { public void printBubbleSortBytecode() throws IOException { var cn = new ClassNode(); - var classFileBytes = Files.readAllBytes(Path.of("build/classes/java/main/org/itmo/lab1/example/BubbleSort.class")); + var classFileBytes = Files.readAllBytes(Path.of("build/classes/java/main/org/example/example/BubbleSort.class")); var classReader = new ClassReader(classFileBytes); classReader.accept(cn, ClassReader.EXPAND_FRAMES); printBytecode(cn); diff --git a/src/main/java/org/example/visitor/ABCMetricsMethodVisitor.java b/src/main/java/org/example/visitor/ABCMetricsMethodVisitor.java new file mode 100644 index 0000000..602615e --- /dev/null +++ b/src/main/java/org/example/visitor/ABCMetricsMethodVisitor.java @@ -0,0 +1,90 @@ +package org.example.visitor; + +import org.example.model.ABCMetrics; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +public class ABCMetricsMethodVisitor extends MethodVisitor { + private final ABCMetrics metrics; + + public ABCMetricsMethodVisitor(ABCMetrics metrics) { + super(Opcodes.ASM9); + this.metrics = metrics; + } + + + @Override + public void visitVarInsn(int opcode, int varIndex) { + if (isStoreInstruction(opcode)) { + metrics.incrementAssignments(); + } + super.visitVarInsn(opcode, varIndex); + } + + @Override + public void visitIincInsn(int varIndex, int increment) { + metrics.incrementAssignments(); + super.visitIincInsn(varIndex, increment); + } + + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + metrics.incrementBranches(); + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + + @Override + public void visitInvokeDynamicInsn( + String name, + String descriptor, + Handle bootstrapMethodHandle, + Object... bootstrapMethodArguments + ) { + metrics.incrementBranches(); + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + } + + @Override + public void visitTypeInsn(int opcode, String type) { + if (opcode == Opcodes.NEW) { + metrics.incrementBranches(); + } + super.visitTypeInsn(opcode, type); + } + + + @Override + public void visitJumpInsn(int opcode, Label label) { + if (opcode != Opcodes.GOTO && opcode != Opcodes.JSR) { + metrics.incrementConditions(); + } + super.visitJumpInsn(opcode, label); + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { + metrics.incrementConditions(labels.length); + super.visitTableSwitchInsn(min, max, dflt, labels); + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + metrics.incrementConditions(labels.length); + super.visitLookupSwitchInsn(dflt, keys, labels); + } + + private boolean isStoreInstruction(int opcode) { + return switch (opcode) { + case Opcodes.ISTORE, + Opcodes.LSTORE, + Opcodes.FSTORE, + Opcodes.DSTORE, + Opcodes.ASTORE -> true; + default -> false; + }; + } +} + diff --git a/src/main/java/org/example/visitor/ClassInfoVisitor.java b/src/main/java/org/example/visitor/ClassInfoVisitor.java new file mode 100644 index 0000000..1c35ac5 --- /dev/null +++ b/src/main/java/org/example/visitor/ClassInfoVisitor.java @@ -0,0 +1,84 @@ +package org.example.visitor; + +import org.example.model.ABCMetrics; +import org.example.model.ClassInfo; +import org.example.model.ClassInfo.Builder; +import org.example.model.MethodInfo; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +public class ClassInfoVisitor extends ClassVisitor { + private final Builder builder; + private final ABCMetrics abcMetrics; + + public ClassInfoVisitor() { + super(Opcodes.ASM9); + this.builder = ClassInfo.builder(); + this.abcMetrics = new ABCMetrics(); + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces + ) { + builder.name(name) + .superName(superName) + .addInterfaces(interfaces) + .isInterface((access & Opcodes.ACC_INTERFACE) != 0); + + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public FieldVisitor visitField( + int access, + String name, + String descriptor, + String signature, + Object value + ) { + builder.incrementFieldCount(); + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod( + int access, + String name, + String descriptor, + String signature, + String[] exceptions + ) { + MethodInfo methodInfo = new MethodInfo(name, descriptor); + builder.addMethod(methodInfo); + + ABCMetrics methodMetrics = new ABCMetrics(); + + return new ABCMetricsMethodVisitor(methodMetrics) { + + @Override + public void visitEnd() { + abcMetrics.add(methodMetrics); + super.visitEnd(); + } + }; + } + + @Override + public void visitEnd() { + builder.abcMetrics(abcMetrics); + super.visitEnd(); + } + + public ClassInfo getClassInfo() { + return builder.build(); + } +} + From 8e64c854f452e3308d2582d94dc60ac47e07148e Mon Sep 17 00:00:00 2001 From: Gogolev Andrey Date: Thu, 4 Dec 2025 00:16:13 +0300 Subject: [PATCH 3/6] add metrics calculator --- .../org/example/service/JarProcessor.java | 7 +- .../example/service/MetricsCalculator.java | 214 ++++++++++++++++++ 2 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/example/service/MetricsCalculator.java diff --git a/src/main/java/org/example/service/JarProcessor.java b/src/main/java/org/example/service/JarProcessor.java index 8ac8ea3..1b0c3de 100644 --- a/src/main/java/org/example/service/JarProcessor.java +++ b/src/main/java/org/example/service/JarProcessor.java @@ -8,7 +8,6 @@ import java.io.InputStream; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.jar.JarEntry; @@ -27,7 +26,10 @@ public List process(Path jarPath) throws IOException { while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); if (isClassFile(entry)) { - classes.add(processClassEntry(jarFile, entry)); + ClassInfo classInfo = processClassEntry(jarFile, entry); + if (classInfo != null) { + classes.add(classInfo); + } } } } @@ -50,6 +52,7 @@ private ClassInfo processClassEntry(JarFile jarFile, JarEntry entry) { return collector.getClassInfo(); } catch (Exception e) { log.log(Level.WARNING, "Failed to process class: " + entry.getName(), e); + return null; } } } diff --git a/src/main/java/org/example/service/MetricsCalculator.java b/src/main/java/org/example/service/MetricsCalculator.java new file mode 100644 index 0000000..9a52e3b --- /dev/null +++ b/src/main/java/org/example/service/MetricsCalculator.java @@ -0,0 +1,214 @@ +package org.example.service; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.example.model.ABCMetrics; +import org.example.model.ClassInfo; +import org.example.model.JarAnalysisResult; +import org.example.model.JarAnalysisResult.ABCSummary; +import org.example.model.JarAnalysisResult.InheritanceMetrics; +import org.example.model.MethodInfo; + +public class MetricsCalculator { + private static final String JAVA_LANG_OBJECT = "java/lang/Object"; + private static final Set OBJECT_METHODS = Set.of( + new MethodInfo("equals", "(Ljava/lang/Object;)Z"), + new MethodInfo("hashCode", "()I"), + new MethodInfo("toString", "()Ljava/lang/String;"), + new MethodInfo("clone", "()Ljava/lang/Object;"), + new MethodInfo("finalize", "()V") + ); + + public JarAnalysisResult calculate(String jarFileName, List classes) { + Map classMap = classes.stream() + .collect(Collectors.toMap( + ClassInfo::getName, + Function.identity(), + (ex, re) -> re + )); + + int interfaceCount = 0; + int classCount = 0; + for (ClassInfo ci : classes) { + if (ci.isInterface()) { + interfaceCount++; + } else { + classCount++; + } + } + + int maxDepth = 0; + int totalDepth = 0; + for (ClassInfo ci : classes) { + int depth = calculateInheritanceDepth(ci, classMap); + totalDepth += depth; + if (depth > maxDepth) { + maxDepth = depth; + } + } + double avgDepth = classes.isEmpty() ? 0 : (double) totalDepth / classes.size(); + + ABCMetrics totalAbc = new ABCMetrics(); + for (ClassInfo ci : classes) { + totalAbc.add(ci.getAbcMetrics()); + } + + int totalOverridden = 0; + int classesWithMethods = 0; + for (ClassInfo ci : classes) { + if (!ci.isInterface()) { + totalOverridden += countOverriddenMethods(ci, classMap); + classesWithMethods++; + } + } + double avgOverridden = classesWithMethods == 0 ? 0 : (double) totalOverridden / classesWithMethods; + + int totalFields = 0; + for (ClassInfo ci : classes) { + totalFields += ci.getFieldCount(); + } + double avgFields = classes.isEmpty() ? 0 : (double) totalFields / classes.size(); + + return new JarAnalysisResult( + jarFileName, + classCount, + interfaceCount, + new InheritanceMetrics(maxDepth, avgDepth), + ABCSummary.from(totalAbc), + avgOverridden, + avgFields + ); + } + + private int calculateInheritanceDepth(ClassInfo classInfo, Map classMap) { + int classChainDepth = calculateClassDepth(classInfo.getSuperName(), classMap); + + int interfaceDepth = 0; + for (String iface : classInfo.getInterfaces()) { + int depth = calculateInterfaceDepth(iface, classMap, 1); + if (depth > interfaceDepth) { + interfaceDepth = depth; + } + } + + return Math.max(classChainDepth, interfaceDepth); + } + + private int calculateClassDepth(String superName, Map classMap) { + int depth = 0; + String current = superName; + + while (current != null && !JAVA_LANG_OBJECT.equals(current)) { + depth++; + ClassInfo superClass = classMap.get(current); + if (superClass != null) { + current = superClass.getSuperName(); + } else { + break; + } + } + + return depth; + } + + private int calculateInterfaceDepth(String interfaceName, Map classMap, int currentDepth) { + if (interfaceName == null) { + return currentDepth; + } + + ClassInfo iface = classMap.get(interfaceName); + if (iface == null) { + return currentDepth; + } + + int maxDepth = currentDepth; + for (String parentInterface : iface.getInterfaces()) { + int depth = calculateInterfaceDepth(parentInterface, classMap, currentDepth + 1); + if (depth > maxDepth) { + maxDepth = depth; + } + } + + return maxDepth; + } + + private int countOverriddenMethods(ClassInfo classInfo, Map classMap) { + Set parentMethods = new HashSet<>(OBJECT_METHODS); + + collectMethodsFromSuperClass(classInfo.getSuperName(), classMap, parentMethods); + + for (String iface : classInfo.getInterfaces()) { + collectMethodsFromInterface(iface, classMap, parentMethods); + } + + int count = 0; + for (MethodInfo method : classInfo.getMethods()) { + if (method.isConstructor() || method.isStaticInitializer()) { + continue; + } + if (parentMethods.contains(method)) { + count++; + } + } + + return count; + } + + private void collectMethodsFromSuperClass( + String superClassName, + Map classMap, + Set result + ) { + if (superClassName == null || JAVA_LANG_OBJECT.equals(superClassName)) { + return; + } + + ClassInfo superClass = classMap.get(superClassName); + if (superClass == null) { + return; + } + + for (MethodInfo method : superClass.getMethods()) { + if (!method.isConstructor() && !method.isStaticInitializer()) { + result.add(method); + } + } + + collectMethodsFromSuperClass(superClass.getSuperName(), classMap, result); + + for (String iface : superClass.getInterfaces()) { + collectMethodsFromInterface(iface, classMap, result); + } + } + + private void collectMethodsFromInterface( + String interfaceName, + Map classMap, + Set result + ) { + if (interfaceName == null) { + return; + } + + ClassInfo iface = classMap.get(interfaceName); + if (iface == null) { + return; + } + + for (MethodInfo method : iface.getMethods()) { + if (!method.isStaticInitializer()) { + result.add(method); + } + } + + for (String parentInterface : iface.getInterfaces()) { + collectMethodsFromInterface(parentInterface, classMap, result); + } + } +} + From 7f6aba94bc16d470fdd4dd207804d14810e095a4 Mon Sep 17 00:00:00 2001 From: Gogolev Andrey Date: Thu, 4 Dec 2025 00:28:21 +0300 Subject: [PATCH 4/6] refactor metrics calculator --- .../example/service/MetricsCalculator.java | 45 ++----------------- 1 file changed, 3 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/example/service/MetricsCalculator.java b/src/main/java/org/example/service/MetricsCalculator.java index 9a52e3b..055f370 100644 --- a/src/main/java/org/example/service/MetricsCalculator.java +++ b/src/main/java/org/example/service/MetricsCalculator.java @@ -86,57 +86,18 @@ public JarAnalysisResult calculate(String jarFileName, List classes) } private int calculateInheritanceDepth(ClassInfo classInfo, Map classMap) { - int classChainDepth = calculateClassDepth(classInfo.getSuperName(), classMap); - - int interfaceDepth = 0; - for (String iface : classInfo.getInterfaces()) { - int depth = calculateInterfaceDepth(iface, classMap, 1); - if (depth > interfaceDepth) { - interfaceDepth = depth; - } - } - - return Math.max(classChainDepth, interfaceDepth); - } - - private int calculateClassDepth(String superName, Map classMap) { - int depth = 0; - String current = superName; + int depth = 1; + String current = classInfo.getSuperName(); while (current != null && !JAVA_LANG_OBJECT.equals(current)) { depth++; ClassInfo superClass = classMap.get(current); - if (superClass != null) { - current = superClass.getSuperName(); - } else { - break; - } + current = (superClass != null) ? superClass.getSuperName() : null; } return depth; } - private int calculateInterfaceDepth(String interfaceName, Map classMap, int currentDepth) { - if (interfaceName == null) { - return currentDepth; - } - - ClassInfo iface = classMap.get(interfaceName); - if (iface == null) { - return currentDepth; - } - - int maxDepth = currentDepth; - for (String parentInterface : iface.getInterfaces()) { - int depth = calculateInterfaceDepth(parentInterface, classMap, currentDepth + 1); - if (depth > maxDepth) { - maxDepth = depth; - } - } - - return maxDepth; - } - private int countOverriddenMethods(ClassInfo classInfo, Map classMap) { Set parentMethods = new HashSet<>(OBJECT_METHODS); From 854e9f32ff0e2e025a972838e2e14ce927426be9 Mon Sep 17 00:00:00 2001 From: Gogolev Andrey Date: Thu, 4 Dec 2025 00:34:18 +0300 Subject: [PATCH 5/6] add main and logging --- build.gradle.kts | 3 +++ src/main/java/org/example/service/JarProcessor.java | 13 +++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a9065d8..b332f43 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,6 +17,9 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") + implementation("org.slf4j:slf4j-api:2.0.9") + implementation("ch.qos.logback:logback-classic:1.4.11") + 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/src/main/java/org/example/service/JarProcessor.java b/src/main/java/org/example/service/JarProcessor.java index 1b0c3de..820bf83 100644 --- a/src/main/java/org/example/service/JarProcessor.java +++ b/src/main/java/org/example/service/JarProcessor.java @@ -3,6 +3,8 @@ import org.example.model.ClassInfo; import org.example.visitor.ClassInfoVisitor; import org.objectweb.asm.ClassReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; @@ -12,11 +14,9 @@ import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; -import java.util.logging.Level; -import java.util.logging.Logger; public class JarProcessor { - private static final Logger log = Logger.getLogger(JarProcessor.class.getName()); + private static final Logger log = LoggerFactory.getLogger(JarProcessor.class); public List process(Path jarPath) throws IOException { List classes = new ArrayList<>(); @@ -34,7 +34,7 @@ public List process(Path jarPath) throws IOException { } } - log.info(() -> String.format("Processed %d classes from %s", classes.size(), jarPath.getFileName())); + log.info("Processed {} classes from {}", classes.size(), jarPath.getFileName()); return classes; } @@ -46,14 +46,11 @@ private ClassInfo processClassEntry(JarFile jarFile, JarEntry entry) { try (InputStream inputStream = jarFile.getInputStream(entry)) { ClassReader classReader = new ClassReader(inputStream); ClassInfoVisitor collector = new ClassInfoVisitor(); - classReader.accept(collector, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); - return collector.getClassInfo(); } catch (Exception e) { - log.log(Level.WARNING, "Failed to process class: " + entry.getName(), e); + log.error("Failed to process class: {}", entry.getName(), e); return null; } } } - From 2e219b5e85bbc4779516bdfb72092b7cc8b3e09b Mon Sep 17 00:00:00 2001 From: Gogolev Andrey Date: Thu, 4 Dec 2025 00:37:03 +0300 Subject: [PATCH 6/6] add main and logging --- src/main/java/org/example/JarAnalyzerApp.java | 90 +++++++++++++++++++ .../java/org/example/model/ClassInfo.java | 1 - src/main/resources/logback.xml | 13 +++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/example/JarAnalyzerApp.java create mode 100644 src/main/resources/logback.xml diff --git a/src/main/java/org/example/JarAnalyzerApp.java b/src/main/java/org/example/JarAnalyzerApp.java new file mode 100644 index 0000000..91ec88b --- /dev/null +++ b/src/main/java/org/example/JarAnalyzerApp.java @@ -0,0 +1,90 @@ +package org.example; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.example.model.ClassInfo; +import org.example.model.JarAnalysisResult; +import org.example.service.JarProcessor; +import org.example.service.MetricsCalculator; +import org.example.service.ReportGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JarAnalyzerApp { + private static final Logger log = LoggerFactory.getLogger(JarAnalyzerApp.class); + + private final JarProcessor jarProcessor; + private final MetricsCalculator metricsCalculator; + private final ReportGenerator reportGenerator; + + public JarAnalyzerApp() { + this.jarProcessor = new JarProcessor(); + this.metricsCalculator = new MetricsCalculator(); + this.reportGenerator = new ReportGenerator(); + } + + public void analyze(String jarPath, String outputPath) { + try { + Path jarFilePath = Path.of(jarPath); + validateInputFile(jarFilePath); + + System.out.println("Processing JAR file: " + jarFilePath.getFileName()); + List classes = jarProcessor.process(jarFilePath); + + if (classes.isEmpty()) { + log.warn("No classes found in the JAR file"); + } + + System.out.println("Calculating metrics..."); + String jarFileName = jarFilePath.getFileName().toString(); + JarAnalysisResult result = metricsCalculator.calculate(jarFileName, classes); + + if (outputPath == null) { + reportGenerator.printToConsole(result); + } else { + writeToFile(result, outputPath); + } + } catch (Exception e) { + log.error("Application run failed", e); + System.exit(1); + } + } + + private void writeToFile(JarAnalysisResult result, String outputPath) throws IOException { + Path outputFilePath = Path.of(outputPath); + reportGenerator.writeToJson(result, outputFilePath); + System.out.println("JSON report written to: " + outputFilePath.toAbsolutePath()); + } + + private void validateInputFile(Path jarPath) { + if (!Files.exists(jarPath)) { + throw new IllegalArgumentException("Input file does not exist: " + jarPath); + } + + if (!Files.isRegularFile(jarPath)) { + throw new IllegalArgumentException("Input path is not a file: " + jarPath); + } + + if (!jarPath.toString().toLowerCase().endsWith(".jar")) { + throw new IllegalArgumentException("Input file must be a JAR file: " + jarPath); + } + } + + public static void main(String[] args) { + if (args.length < 1 || args.length > 2) { + System.out.println("Usage:"); + System.out.println("java -jar analyzer.jar - output to console"); + System.out.println("java -jar analyzer.jar - output to file"); + System.exit(1); + } + + String inputJar = args[0]; + String outputJson = args.length == 2 ? args[1] : null; + + JarAnalyzerApp app = new JarAnalyzerApp(); + app.analyze(inputJar, outputJson); + } +} diff --git a/src/main/java/org/example/model/ClassInfo.java b/src/main/java/org/example/model/ClassInfo.java index 13acef1..b8f05e7 100644 --- a/src/main/java/org/example/model/ClassInfo.java +++ b/src/main/java/org/example/model/ClassInfo.java @@ -1,7 +1,6 @@ package org.example.model; import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..d2b94aa --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + + System.err + + %d{HH:mm:ss.SSS} [%level] %logger{36} - %msg%n + + + + + + + \ No newline at end of file