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/analyzer/build.gradle.kts b/analyzer/build.gradle.kts new file mode 100644 index 0000000..6302e1f --- /dev/null +++ b/analyzer/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("java-library") +} + +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") + + 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() +} diff --git a/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/Analyzer.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/Analyzer.java new file mode 100644 index 0000000..00372b1 --- /dev/null +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/Analyzer.java @@ -0,0 +1,57 @@ +package com.quicklybly.itmo.bytecodeanalyzer; + +import com.quicklybly.itmo.bytecodeanalyzer.api.Statistic; +import com.quicklybly.itmo.bytecodeanalyzer.util.ClassRepository; +import com.quicklybly.itmo.bytecodeanalyzer.util.provider.ClassProvider; +import com.quicklybly.itmo.bytecodeanalyzer.util.provider.impl.CompositeClassProvider; +import com.quicklybly.itmo.bytecodeanalyzer.util.provider.impl.JarClassProvider; +import com.quicklybly.itmo.bytecodeanalyzer.util.provider.impl.SystemClassProvider; +import com.quicklybly.itmo.bytecodeanalyzer.visitor.ClassStatisticVisitor; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.List; +import java.util.jar.JarFile; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; + +public class Analyzer { + + public static void main(String[] args) throws IOException { + var path = Path.of("analyzer/src/main/resources/sample.jar"); + var analyzer = new Analyzer(); + + var statistic = analyzer.analyzeJar(path); + System.out.println(statistic); + } + + public Statistic analyzeJar(Path path) throws IOException { + Statistic statistic = new Statistic(); + try (JarFile jar = new JarFile(path.toFile())) { + + ClassProvider jarProvider = new JarClassProvider(jar); + ClassProvider systemProvider = new SystemClassProvider(); + ClassProvider compositeProvider = new CompositeClassProvider(List.of(jarProvider, systemProvider)); + + ClassRepository repository = new ClassRepository(compositeProvider); + + + jar.stream() + .filter(entry -> entry.getName().endsWith(".class")) + .forEach(entry -> { + String internalName = entry.getName().replace(".class", ""); + + try (InputStream is = jar.getInputStream(entry)) { + ClassReader cr = new ClassReader(is); + + ClassVisitor visitor = new ClassStatisticVisitor(statistic, repository); + + cr.accept(visitor, 0); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + return statistic; + } +} diff --git a/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/api/Statistic.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/api/Statistic.java new file mode 100644 index 0000000..bdeb291 --- /dev/null +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/api/Statistic.java @@ -0,0 +1,57 @@ +package com.quicklybly.itmo.bytecodeanalyzer.api; + +import com.quicklybly.itmo.bytecodeanalyzer.model.AbcMetric; +import com.quicklybly.itmo.bytecodeanalyzer.model.AverageHolder; + +public class Statistic { + + private final AverageHolder fieldAverageHolder = new AverageHolder(); + private final AverageHolder averageInheritanceDepthHolder = new AverageHolder(); + private final AverageHolder averageOverriddenCountHolder = new AverageHolder(); + private int maxInheritanceDepth = 0; + private final AbcMetric abcMetric = new AbcMetric(0, 0, 0); + + public void updateFieldStatistic(Integer fieldCount) { + fieldAverageHolder.updateAverage(fieldCount); + } + + public void writeDepth(int depth) { + averageInheritanceDepthHolder.updateAverage(depth); + maxInheritanceDepth = Math.max(maxInheritanceDepth, depth); + } + + public void writeOverriddenCount(int overriddenCount) { + averageOverriddenCountHolder.updateAverage(overriddenCount); + } + + @Override + public String toString() { + return "Statistic{" + + "fieldAverageHolder=" + fieldAverageHolder + + ", averageInheritanceDepthHolder=" + averageInheritanceDepthHolder + + ", averageOverriddenCountHolder=" + averageOverriddenCountHolder + + ", maxInheritanceDepth=" + maxInheritanceDepth + + ", abcMetric=" + abcMetric + + '}'; + } + + public AbcMetric getAbcMetric() { + return abcMetric; + } + + public double getAverageFieldCount() { + return fieldAverageHolder.getAverage(); + } + + public double getAverageInheritanceDepth() { + return averageInheritanceDepthHolder.getAverage(); + } + + public double getAverageOverriddenCount() { + return averageOverriddenCountHolder.getAverage(); + } + + public int getMaxInheritanceDepth() { + return maxInheritanceDepth; + } +} diff --git a/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/api/StatisticMapper.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/api/StatisticMapper.java new file mode 100644 index 0000000..8bc9c9d --- /dev/null +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/api/StatisticMapper.java @@ -0,0 +1,25 @@ +package com.quicklybly.itmo.bytecodeanalyzer.api; + +public class StatisticMapper { + + private StatisticMapper() { + throw new IllegalStateException("Utility class"); + } + + public static String toJson(Statistic statistic) { + return String.format( + "{" + + "\"fieldAverage\":%s," + + "\"averageInheritanceDepth\":%s," + + "\"averageOverriddenCountHolder\":%s," + + "\"maxInheritanceDepth\":%d," + + "\"abc\":%s" + + "}", + statistic.getAverageFieldCount(), + statistic.getAverageInheritanceDepth(), + statistic.getAverageOverriddenCount(), + statistic.getMaxInheritanceDepth(), + statistic.getAbcMetric().getAbc() + ); + } +} diff --git a/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/model/AbcMetric.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/model/AbcMetric.java new file mode 100644 index 0000000..98a301a --- /dev/null +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/model/AbcMetric.java @@ -0,0 +1,68 @@ +package com.quicklybly.itmo.bytecodeanalyzer.model; + +/* + ABC rules for Java + The following rules give the count of Assignments, Branches, Conditionals in the ABC metric for Java: + + 1. Add one to the assignment count when: + Occurrence of an assignment operator (exclude constant declarations and default parameter assignments) (**=, *=, /=, %=, +=, -=, <<=, >>=, &=, !=, ^=, >>>=**). + Occurrence of an increment or a decrement operator (prefix or postfix) (++, --). + 2. Add one to branch count when + Occurrence of a function call or a class method call. + Occurrence of a ‘new’ operator. + 3. Add one to condition count when: + Occurrence of a conditional operator (<, >, <=, >=, ==, !=). + Occurrence of the following keywords (‘else’, ‘case’, ‘default’, ‘?’, ‘try’, ‘catch’). + Occurrence of a unary conditional operator. + */ +public class AbcMetric { + + private long assignment; + private long branch; + private long condition; + + public AbcMetric(long assignment, long branch, long condition) { + this.assignment = assignment; + this.branch = branch; + this.condition = condition; + } + + public AbcMetric(AbcMetric other) { + this.assignment = other.assignment; + this.branch = other.branch; + this.condition = other.condition; + } + + public void add(AbcMetric other) { + this.assignment += other.assignment; + this.branch += other.branch; + this.condition += other.condition; + } + + public double getAbc() { + return Math.sqrt(assignment * assignment + branch * branch + condition * condition); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + + AbcMetric abcMetric = (AbcMetric) o; + return assignment == abcMetric.assignment && branch == abcMetric.branch && condition == abcMetric.condition; + } + + @Override + public int hashCode() { + int result = Long.hashCode(assignment); + result = 31 * result + Long.hashCode(branch); + result = 31 * result + Long.hashCode(condition); + return result; + } + + @Override + public String toString() { + return "AbcMetric{" + + "abc=" + getAbc() + + '}'; + } +} diff --git a/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/model/AverageHolder.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/model/AverageHolder.java new file mode 100644 index 0000000..3bc1ea1 --- /dev/null +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/model/AverageHolder.java @@ -0,0 +1,25 @@ +package com.quicklybly.itmo.bytecodeanalyzer.model; + +public class AverageHolder { + private final Object lock = new Object(); + private Integer sum = 0; + private Integer count = 0; + + public Double getAverage() { + return count.equals(0) ? 0.0 : sum.doubleValue() / count; + } + + public void updateAverage(Integer value) { + synchronized (lock) { + sum += value; + count++; + } + } + + @Override + public String toString() { + return "AverageHolder{" + + "average=" + getAverage() + + '}'; + } +} diff --git a/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/model/hierarchy/ClassHierarchyNode.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/model/hierarchy/ClassHierarchyNode.java new file mode 100644 index 0000000..b15f865 --- /dev/null +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/model/hierarchy/ClassHierarchyNode.java @@ -0,0 +1,55 @@ +package com.quicklybly.itmo.bytecodeanalyzer.model.hierarchy; + +import java.util.Collections; +import java.util.Set; + +public class ClassHierarchyNode { + + private final String name; + private final String superName; + private final Set methods; // name + descriptor + + private Integer cachedDepth = null; + + public ClassHierarchyNode(String name, String superName, Set methods) { + if (name == null || superName == null) { + throw new IllegalArgumentException("Name and superName must not be null"); + } + this.name = name; + this.superName = superName; + this.methods = methods != null ? methods : Collections.emptySet(); + } + + public String getName() { + return name; + } + + public String getSuperName() { + return superName; + } + + public boolean hasMethod(String signature) { + return methods.contains(signature); + } + + public void setCachedDepth(int depth) { + this.cachedDepth = depth; + } + + public Integer getCachedDepth() { + return cachedDepth; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + + ClassHierarchyNode that = (ClassHierarchyNode) o; + return name.equals(that.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/util/ByteCodePrinter.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/ByteCodePrinter.java similarity index 98% rename from src/main/java/org/example/util/ByteCodePrinter.java rename to analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/ByteCodePrinter.java index 3d1ca38..d74b95c 100644 --- a/src/main/java/org/example/util/ByteCodePrinter.java +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/ByteCodePrinter.java @@ -1,4 +1,4 @@ -package org.example.util; +package com.quicklybly.itmo.bytecodeanalyzer.util; import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.ClassNode; diff --git a/src/main/java/org/example/util/CheckFrameAnalyzer.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/CheckFrameAnalyzer.java similarity index 99% rename from src/main/java/org/example/util/CheckFrameAnalyzer.java rename to analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/CheckFrameAnalyzer.java index 5231ca7..c312d9e 100644 --- a/src/main/java/org/example/util/CheckFrameAnalyzer.java +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/CheckFrameAnalyzer.java @@ -1,4 +1,4 @@ -package org.example.util; +package com.quicklybly.itmo.bytecodeanalyzer.util; // ASM: a very small and fast Java bytecode manipulation framework // Copyright (c) 2000-2011 INRIA, France Telecom diff --git a/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/ClassRepository.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/ClassRepository.java new file mode 100644 index 0000000..f3ececd --- /dev/null +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/ClassRepository.java @@ -0,0 +1,127 @@ +package com.quicklybly.itmo.bytecodeanalyzer.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import com.quicklybly.itmo.bytecodeanalyzer.model.hierarchy.ClassHierarchyNode; +import com.quicklybly.itmo.bytecodeanalyzer.util.provider.ClassProvider; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +public class ClassRepository { + + private static final String OBJECT_CLASS_NAME = "java/lang/Object"; + + private final Map cache = new HashMap<>(); + private final ClassProvider classProvider; + + public ClassRepository(ClassProvider classProvider) { + this.classProvider = classProvider; + } + + public ClassHierarchyNode getNode(String className) { + if (className == null) { + throw new IllegalArgumentException("Class name must not be null"); + } + + if (cache.containsKey(className)) { + return cache.get(className); + } + + try { + ClassHierarchyNode node = loadNode(className); + cache.put(className, node); + return node; + } catch (IOException e) { + System.err.println("WARN: Unable to load class " + className); + cache.put(className, null); // remember failed attempt + return null; + } + } + + public int getDepth(String className) { + if (OBJECT_CLASS_NAME.equals(className) || className == null) { + return 0; + } + + ClassHierarchyNode node = getNode(className); + + if (node == null) { + return 0; + } + + if (node.getCachedDepth() != null) { + return node.getCachedDepth(); + } + + int depth = 1 + getDepth(node.getSuperName()); + + node.setCachedDepth(depth); + return depth; + } + + public boolean isOverridden(String className, String methodSignature) { + ClassHierarchyNode node = getNode(className); + + if (node == null) { + return false; + } + + return checkParentHierarchy(node.getSuperName(), methodSignature); + } + + private boolean checkParentHierarchy(String currentClassName, String methodSignature) { + if (currentClassName == null || OBJECT_CLASS_NAME.equals(currentClassName)) { + return false; + } + + ClassHierarchyNode node = getNode(currentClassName); + if (node == null) { + return false; + } + + if (node.hasMethod(methodSignature)) { + return true; + } + + return checkParentHierarchy(node.getSuperName(), methodSignature); + } + + private ClassHierarchyNode loadNode(String className) throws IOException { + try (InputStream in = classProvider.getClassInputStream(className)) { + if (in == null) return null; + + ClassReader cr = new ClassReader(in); + Set methods = new HashSet<>(); + final String[] superNameHolder = new String[1]; + + // only reading superclass and method signature + // flags needed for performance + cr.accept(new ClassVisitor(Opcodes.ASM9) { + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + superNameHolder[0] = superName; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + boolean isPrivateOrStatic = (access & (Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC)) != 0; + boolean isConstructor = name.startsWith("<"); + + if (!isPrivateOrStatic && !isConstructor) { + methods.add(name + descriptor); + } + return null; // not entering method body + } + }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + + return new ClassHierarchyNode(className, superNameHolder[0], methods); + } + } +} diff --git a/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/provider/ClassProvider.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/provider/ClassProvider.java new file mode 100644 index 0000000..65d3f87 --- /dev/null +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/provider/ClassProvider.java @@ -0,0 +1,14 @@ +package com.quicklybly.itmo.bytecodeanalyzer.util.provider; + +import java.io.IOException; +import java.io.InputStream; + +@FunctionalInterface +public interface ClassProvider { + + /** + * @param internalName class name in ASM format, for example "java/lang/String" + * @return InputStream or null + */ + InputStream getClassInputStream(String internalName) throws IOException; +} diff --git a/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/provider/impl/CompositeClassProvider.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/provider/impl/CompositeClassProvider.java new file mode 100644 index 0000000..3a7de0d --- /dev/null +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/provider/impl/CompositeClassProvider.java @@ -0,0 +1,27 @@ +package com.quicklybly.itmo.bytecodeanalyzer.util.provider.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import com.quicklybly.itmo.bytecodeanalyzer.util.provider.ClassProvider; + +public class CompositeClassProvider implements ClassProvider { + + private final List providers; + + public CompositeClassProvider(List providers) { + this.providers = providers; + } + + @Override + public InputStream getClassInputStream(String internalName) throws IOException { + for (ClassProvider provider : providers) { + InputStream is = provider.getClassInputStream(internalName); + + if (is != null) { + return is; + } + } + return null; + } +} diff --git a/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/provider/impl/JarClassProvider.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/provider/impl/JarClassProvider.java new file mode 100644 index 0000000..8d7cd03 --- /dev/null +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/provider/impl/JarClassProvider.java @@ -0,0 +1,28 @@ +package com.quicklybly.itmo.bytecodeanalyzer.util.provider.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import com.quicklybly.itmo.bytecodeanalyzer.util.provider.ClassProvider; + +public class JarClassProvider implements ClassProvider { + + private final JarFile jarFile; + + public JarClassProvider(JarFile jarFile) { + this.jarFile = jarFile; + } + + @Override + public InputStream getClassInputStream(String internalName) throws IOException { + String entryName = internalName + ".class"; + ZipEntry entry = jarFile.getEntry(entryName); + + if (entry == null) { + return null; + } + + return jarFile.getInputStream(entry); + } +} diff --git a/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/provider/impl/SystemClassProvider.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/provider/impl/SystemClassProvider.java new file mode 100644 index 0000000..032515b --- /dev/null +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/util/provider/impl/SystemClassProvider.java @@ -0,0 +1,13 @@ +package com.quicklybly.itmo.bytecodeanalyzer.util.provider.impl; + +import java.io.InputStream; +import com.quicklybly.itmo.bytecodeanalyzer.util.provider.ClassProvider; + +public class SystemClassProvider implements ClassProvider { + + @Override + public InputStream getClassInputStream(String internalName) { + String resourceName = internalName + ".class"; + return ClassLoader.getSystemResourceAsStream(resourceName); + } +} diff --git a/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/visitor/AbcMethodVisitor.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/visitor/AbcMethodVisitor.java new file mode 100644 index 0000000..fc23b21 --- /dev/null +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/visitor/AbcMethodVisitor.java @@ -0,0 +1,108 @@ +package com.quicklybly.itmo.bytecodeanalyzer.visitor; + +import com.quicklybly.itmo.bytecodeanalyzer.model.AbcMetric; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import static org.objectweb.asm.Opcodes.ASM9; + +// init blocks are inlined in constructor +public class AbcMethodVisitor extends MethodVisitor { + + private final AbcMetric resultMetric; + private long assignment; + private long branch; + private long condition; + + public AbcMethodVisitor(MethodVisitor delegate, AbcMetric resultMetric) { + super(ASM9, delegate); + this.resultMetric = resultMetric; + } + + @Override + public void visitVarInsn(int opcode, int varIndex) { + if (opcode >= Opcodes.ISTORE && opcode <= Opcodes.ASTORE) { + assignment++; + } + + super.visitVarInsn(opcode, varIndex); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + if (opcode == Opcodes.PUTSTATIC || opcode == Opcodes.PUTFIELD) { + assignment++; + } + + super.visitFieldInsn(opcode, owner, name, descriptor); + } + + @Override + public void visitIincInsn(int varIndex, int increment) { + assignment++; + super.visitIincInsn(varIndex, increment); + } + + @Override + public void visitInsn(int opcode) { + // array assignment + if (opcode >= Opcodes.IASTORE && opcode <= Opcodes.SASTORE) { + assignment++; + } + + super.visitInsn(opcode); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + branch++; + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + branch++; + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + if (opcode != Opcodes.GOTO && opcode != Opcodes.JSR) { + condition++; + } + super.visitJumpInsn(opcode, label); + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { + // dflt = default branch + // labels.length = number of cases + condition += labels.length; // cases + if (dflt != null) { + condition++; + } + super.visitTableSwitchInsn(min, max, dflt, labels); + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + condition += labels.length; // cases + if (dflt != null) { + condition++; + } + super.visitLookupSwitchInsn(dflt, keys, labels); + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + condition++; + super.visitTryCatchBlock(start, end, handler, type); + } + + @Override + public void visitEnd() { + resultMetric.add(new AbcMetric(assignment, branch, condition)); + super.visitEnd(); + } +} diff --git a/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/visitor/ClassStatisticVisitor.java b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/visitor/ClassStatisticVisitor.java new file mode 100644 index 0000000..aea575c --- /dev/null +++ b/analyzer/src/main/java/com/quicklybly/itmo/bytecodeanalyzer/visitor/ClassStatisticVisitor.java @@ -0,0 +1,66 @@ +package com.quicklybly.itmo.bytecodeanalyzer.visitor; + +import com.quicklybly.itmo.bytecodeanalyzer.api.Statistic; +import com.quicklybly.itmo.bytecodeanalyzer.util.ClassRepository; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import static org.objectweb.asm.Opcodes.ASM9; + +public class ClassStatisticVisitor extends ClassVisitor { + + private final Statistic statistic; + private final ClassRepository repository; + + private String className; + private int fieldCount = 0; + private int overriddenCount = 0; + + public ClassStatisticVisitor(Statistic statistic, ClassRepository repository) { + super(ASM9); + this.statistic = statistic; + this.repository = repository; + } + + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + this.className = name; + + int depth = repository.getDepth(name); + statistic.writeDepth(depth); + + super.visit(version, access, name, signature, superName, interfaces); + } + + public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { + fieldCount++; + return super.visitField(access, name, desc, signature, value); + } + + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); + + boolean isPrivate = (access & Opcodes.ACC_PRIVATE) != 0; + boolean isStatic = (access & Opcodes.ACC_STATIC) != 0; + + if (!isPrivate && !isStatic && !name.startsWith("<")) { + String methodSig = name + desc; + if (repository.isOverridden(className, methodSig)) { + overriddenCount++; + } + } + + boolean hasCode = (access & Opcodes.ACC_ABSTRACT) == 0 && (access & Opcodes.ACC_NATIVE) == 0; + + if (!hasCode) { + return mv; + } + + return new AbcMethodVisitor(mv, statistic.getAbcMetric()); + } + + public void visitEnd() { + statistic.updateFieldStatistic(fieldCount); + statistic.writeOverriddenCount(overriddenCount); + } +} diff --git a/src/main/resources/fescar.zip b/analyzer/src/main/resources/fescar.zip similarity index 100% rename from src/main/resources/fescar.zip rename to analyzer/src/main/resources/fescar.zip diff --git a/src/main/resources/guava.zip b/analyzer/src/main/resources/guava.zip similarity index 100% rename from src/main/resources/guava.zip rename to analyzer/src/main/resources/guava.zip diff --git a/src/main/resources/sample.jar b/analyzer/src/main/resources/sample.jar similarity index 100% rename from src/main/resources/sample.jar rename to analyzer/src/main/resources/sample.jar diff --git a/src/main/resources/sample.kt b/analyzer/src/main/resources/sample.kt similarity index 100% rename from src/main/resources/sample.kt rename to analyzer/src/main/resources/sample.kt diff --git a/build.gradle.kts b/build.gradle.kts index 2b92afd..5bc764f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,24 +2,11 @@ plugins { id("java") } -group = "org.example" -version = "1.0-SNAPSHOT" +allprojects { + group = "org.example" + version = "1.0-SNAPSHOT" -repositories { - mavenCentral() + repositories { + 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") - - 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() -} \ No newline at end of file diff --git a/example/build.gradle.kts b/example/build.gradle.kts new file mode 100644 index 0000000..b42849d --- /dev/null +++ b/example/build.gradle.kts @@ -0,0 +1,29 @@ +import org.gradle.jvm.tasks.Jar + +plugins { + id("java") +} + +dependencies { + implementation(project(":analyzer")) + + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.assertj:assertj-core:3.24.2") + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +val jarTask = tasks.named("jar") { + manifest { + attributes("Main-Class" to "org.example.target.Main") + } + archiveFileName.set("example-target.jar") +} + +tasks.test { + useJUnitPlatform() + + dependsOn(jarTask) + + systemProperty("test.jar.path", jarTask.get().archiveFile.get().asFile.absolutePath) +} diff --git a/example/src/main/java/com/quicklybly/itmo/bytecodeanalizer/example/ComplexClass.java b/example/src/main/java/com/quicklybly/itmo/bytecodeanalizer/example/ComplexClass.java new file mode 100644 index 0000000..1244aaf --- /dev/null +++ b/example/src/main/java/com/quicklybly/itmo/bytecodeanalizer/example/ComplexClass.java @@ -0,0 +1,23 @@ +package com.quicklybly.itmo.bytecodeanalizer.example; + +import java.util.ArrayList; + +public class ComplexClass extends ArrayList { + + private final int x = 0; + private final int y = 0; + + public void heavyMethod() { + if (size() > 10) { + System.out.println("List is big"); + } else { + System.out.println("List is small"); + } + new Thread(() -> System.out.println("Async")).start(); + } + + @Override + public int size() { + return super.size(); + } +} diff --git a/example/src/main/java/com/quicklybly/itmo/bytecodeanalizer/example/SimpleClass.java b/example/src/main/java/com/quicklybly/itmo/bytecodeanalizer/example/SimpleClass.java new file mode 100644 index 0000000..c8137a4 --- /dev/null +++ b/example/src/main/java/com/quicklybly/itmo/bytecodeanalizer/example/SimpleClass.java @@ -0,0 +1,5 @@ +package com.quicklybly.itmo.bytecodeanalizer.example; + +public class SimpleClass { + private final String s = "123"; +} diff --git a/example/src/test/java/com/quicklybly/itmo/bytecodeanalizer/example/IntegrationTest.java b/example/src/test/java/com/quicklybly/itmo/bytecodeanalizer/example/IntegrationTest.java new file mode 100644 index 0000000..804d80e --- /dev/null +++ b/example/src/test/java/com/quicklybly/itmo/bytecodeanalizer/example/IntegrationTest.java @@ -0,0 +1,36 @@ +package com.quicklybly.itmo.bytecodeanalizer.example; + +import com.quicklybly.itmo.bytecodeanalyzer.Analyzer; +import com.quicklybly.itmo.bytecodeanalyzer.api.Statistic; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +public class IntegrationTest { + + @Test + public void analyzeComplexClass() throws IOException { + String jarPathStr = System.getProperty("test.jar.path"); + + Assertions.assertNotNull(jarPathStr, "System property 'test.jar.path' is not set"); + + Path jarPath = Path.of(jarPathStr); + File jarFile = jarPath.toFile(); + + System.out.println("Testing artifact: " + jarFile.getAbsolutePath()); + Assertions.assertTrue(jarFile.exists(), "JAR file does not exist at path: " + jarPath); + + Analyzer analyzer = new Analyzer(); + + Statistic stats = analyzer.analyzeJar(jarPath); + + assertThat(stats.getMaxInheritanceDepth()).isEqualTo(4); + assertThat(stats.getAverageInheritanceDepth()).isEqualTo(2.5); + assertThat(stats.getAverageFieldCount()).isEqualTo(1.5); + assertThat(stats.getAverageOverriddenCount()).isEqualTo(0.5); + assertThat(stats.getAbcMetric().getAbc()).isBetween(10.0, 11.0); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index a7f83d7..1665c95 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,3 @@ -rootProject.name = "bytecode-template" \ No newline at end of file +rootProject.name = "bytecode-template" + +include("analyzer", "example") 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/example/BubbleSort.java b/src/main/java/org/example/example/BubbleSort.java deleted file mode 100644 index 8a21c95..0000000 --- a/src/main/java/org/example/example/BubbleSort.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.example.example; - -public class BubbleSort { - - static void bubbleSort(int[] arr, int n) - { - int i, j, temp; - boolean swapped; - for (i = 0; i < n - 1; i++) { - swapped = false; - for (j = 0; j < n - i - 1; j++) { - if (arr[j] > arr[j + 1]) { - - // Swap arr[j] and arr[j+1] - temp = arr[j]; - arr[j] = arr[j + 1]; - arr[j + 1] = temp; - swapped = true; - } - } - - // If no two elements were - // swapped by inner loop, then break - if (!swapped) - break; - } - } - -} 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("}"); - } -} -