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..2ca30db 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation("org.ow2.asm:asm-tree:9.5") implementation("org.ow2.asm:asm-analysis:9.5") implementation("org.ow2.asm:asm-util:9.5") + implementation("com.google.code.gson:gson:2.10.1") testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/config.json b/config.json new file mode 100644 index 0000000..0226c4b --- /dev/null +++ b/config.json @@ -0,0 +1,5 @@ +{ + "inputJar": "src/main/resources/sample.jar", + "outputJson": "metrics.json" +} + diff --git a/metrics.json b/metrics.json new file mode 100644 index 0000000..e1f863f --- /dev/null +++ b/metrics.json @@ -0,0 +1,7 @@ +{ + "Max Depth": 1, + "Average Depth": 0.63, + "Average ABC Metric": 1.15, + "Average Overridden Methods": 0.00, + "Average Fields per Class": 0.38 +} diff --git a/src/main/java/org/example/ClassMetrics.java b/src/main/java/org/example/ClassMetrics.java new file mode 100644 index 0000000..53db538 --- /dev/null +++ b/src/main/java/org/example/ClassMetrics.java @@ -0,0 +1,50 @@ +package org.example; + +public class ClassMetrics { + private final String className; + private int depth; + private double abcMetric; + private int overriddenMethodsCount; + private int fieldsCount; + + public ClassMetrics(String className) { + this.className = className; + this.depth = 0; + this.abcMetric = 0.0; + this.overriddenMethodsCount = 0; + this.fieldsCount = 0; + } + + public int getDepth() { + return depth; + } + + public void setDepth(int depth) { + this.depth = depth; + } + + public double getAbcMetric() { + return abcMetric; + } + + public void setAbcMetric(double abcMetric) { + this.abcMetric = abcMetric; + } + + public int getOverriddenMethodsCount() { + return overriddenMethodsCount; + } + + public void setOverriddenMethodsCount(int overriddenMethodsCount) { + this.overriddenMethodsCount = overriddenMethodsCount; + } + + public int getFieldsCount() { + return fieldsCount; + } + + public void setFieldsCount(int fieldsCount) { + this.fieldsCount = fieldsCount; + } +} + diff --git a/src/main/java/org/example/Example.java b/src/main/java/org/example/Example.java index 52d0abe..61601c9 100644 --- a/src/main/java/org/example/Example.java +++ b/src/main/java/org/example/Example.java @@ -1,6 +1,6 @@ package org.example; -import org.example.visitor.ClassPrinter; +import org.example.visitorы.ClassPrinter; import org.objectweb.asm.ClassReader; import java.io.IOException; diff --git a/src/main/java/org/example/JarAnalyzer.java b/src/main/java/org/example/JarAnalyzer.java new file mode 100644 index 0000000..654e791 --- /dev/null +++ b/src/main/java/org/example/JarAnalyzer.java @@ -0,0 +1,32 @@ +package org.example; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class JarAnalyzer { + private final String jarPath; + + public JarAnalyzer(String jarPath) { + this.jarPath = jarPath; + } + + public void processClasses(ClassVisitor visitor) throws IOException { + try (JarFile jarFile = new JarFile(jarPath)) { + Enumeration entries = jarFile.entries(); + + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.getName().endsWith(".class")) { + ClassReader classReader = new ClassReader(jarFile.getInputStream(entry)); + classReader.accept(visitor, ClassReader.EXPAND_FRAMES); + } + } + } + } +} + diff --git a/src/main/java/org/example/MetricsAnalyzer.java b/src/main/java/org/example/MetricsAnalyzer.java new file mode 100644 index 0000000..6886e1c --- /dev/null +++ b/src/main/java/org/example/MetricsAnalyzer.java @@ -0,0 +1,84 @@ +package org.example; + +import org.example.consoler.Config; +import org.example.consoler.Resulter; +import org.example.misc.ConfigReader; +import org.example.misc.JsonMarshaler; +import org.example.visitorы.*; + +import java.io.IOException; +import java.util.List; + +public class MetricsAnalyzer { + + public static void main(String[] args) { + try { + Config config = ConfigReader.readConfig(); + Resulter metrics = analyzeJar(config.getInputJar()); + print(metrics); + JsonMarshaler.write(metrics, config.getOutputJson()); + } catch (Exception e) { + System.err.println("Error: " + e.getMessage()); + e.printStackTrace(); + } + } + + private static Resulter analyzeJar(String jarPath) throws IOException { + JarAnalyzer jarAnalyzer = new JarAnalyzer(jarPath); + MultiVisitor multiVisitor = new MultiVisitor(); + + jarAnalyzer.processClasses(multiVisitor); + List classMetricsList = multiVisitor.getClassMetricsList(); + + return calculateAggregatedMetrics(classMetricsList); + } + + private static Resulter calculateAggregatedMetrics( + List classMetricsList) { + + if (classMetricsList.isEmpty()) { + return new Resulter(0, 0.0, 0.0, 0.0, 0.0); + } + + int maxInheritanceDepth = 0; + double totalInheritanceDepth = 0; + double totalABCMetric = 0; + double totalOverriddenMethods = 0; + double totalFields = 0; + int classCount = 0; + + for (ClassMetrics cm : classMetricsList) { + if (cm.getDepth() > maxInheritanceDepth) { + maxInheritanceDepth = cm.getDepth(); + } + totalInheritanceDepth += cm.getDepth(); + totalABCMetric += cm.getAbcMetric(); + totalOverriddenMethods += cm.getOverriddenMethodsCount(); + totalFields += cm.getFieldsCount(); + classCount++; + } + + double avgInheritanceDepth = totalInheritanceDepth / classCount; + double avgABCMetric = totalABCMetric / classCount; + double avgOverriddenMethods = totalOverriddenMethods / classCount; + double avgFieldsPerClass = totalFields / classCount; + + return new Resulter( + maxInheritanceDepth, + avgInheritanceDepth, + avgABCMetric, + avgOverriddenMethods, + avgFieldsPerClass + ); + } + + private static void print(Resulter metrics) { + System.out.println("====Metrics====\n"); + System.out.println("Maximum Depth: " + metrics.getMaxDepth()); + System.out.println("Average Depth: " + String.format("%.2f", metrics.getAvgDepth())); + System.out.println("Average ABC Metric: " + String.format("%.2f", metrics.getAvgABCMetric())); + System.out.println("Average Overridden Methods: " + String.format("%.2f", metrics.getAvgOverriddenMethods())); + System.out.println("Average Fields per Class: " + String.format("%.2f", metrics.getAvgFieldsPerClass())); + } +} + diff --git a/src/main/java/org/example/consoler/Config.java b/src/main/java/org/example/consoler/Config.java new file mode 100644 index 0000000..2d069a1 --- /dev/null +++ b/src/main/java/org/example/consoler/Config.java @@ -0,0 +1,23 @@ +package org.example.consoler; + +public class Config { + private String inputJar; + private String outputJson; + + public Config() { + } + + public Config(String inputJar, String outputJson) { + this.inputJar = inputJar; + this.outputJson = outputJson; + } + + public String getInputJar() { + return inputJar; + } + + public String getOutputJson() { + return outputJson; + } +} + diff --git a/src/main/java/org/example/consoler/Resulter.java b/src/main/java/org/example/consoler/Resulter.java new file mode 100644 index 0000000..b0461b7 --- /dev/null +++ b/src/main/java/org/example/consoler/Resulter.java @@ -0,0 +1,40 @@ +package org.example.consoler; + +public class Resulter { + private final int maxDepth; + private final double avgDepth; + private final double avgABCMetric; + private final double avgOverriddenMethods; + private final double avgFieldsPerClass; + + public Resulter(int maxDepth, double avgDepth, + double avgABCMetric, double avgOverriddenMethods, + double avgFieldsPerClass) { + this.maxDepth = maxDepth; + this.avgDepth = avgDepth; + this.avgABCMetric = avgABCMetric; + this.avgOverriddenMethods = avgOverriddenMethods; + this.avgFieldsPerClass = avgFieldsPerClass; + } + + public int getMaxDepth() { + return maxDepth; + } + + public double getAvgDepth() { + return avgDepth; + } + + public double getAvgABCMetric() { + return avgABCMetric; + } + + public double getAvgOverriddenMethods() { + return avgOverriddenMethods; + } + + public double getAvgFieldsPerClass() { + return avgFieldsPerClass; + } +} + diff --git a/src/main/java/org/example/util/ByteCodePrinter.java b/src/main/java/org/example/misc/ByteCodePrinter.java similarity index 99% rename from src/main/java/org/example/util/ByteCodePrinter.java rename to src/main/java/org/example/misc/ByteCodePrinter.java index 3d1ca38..24ae433 100644 --- a/src/main/java/org/example/util/ByteCodePrinter.java +++ b/src/main/java/org/example/misc/ByteCodePrinter.java @@ -1,4 +1,4 @@ -package org.example.util; +package org.example.misc; import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.ClassNode; diff --git a/src/main/java/org/example/util/CheckFrameAnalyzer.java b/src/main/java/org/example/misc/CheckFrameAnalyzer.java similarity index 99% rename from src/main/java/org/example/util/CheckFrameAnalyzer.java rename to src/main/java/org/example/misc/CheckFrameAnalyzer.java index 5231ca7..900bd8d 100644 --- a/src/main/java/org/example/util/CheckFrameAnalyzer.java +++ b/src/main/java/org/example/misc/CheckFrameAnalyzer.java @@ -1,4 +1,4 @@ -package org.example.util; +package org.example.misc; // ASM: a very small and fast Java bytecode manipulation framework // Copyright (c) 2000-2011 INRIA, France Telecom diff --git a/src/main/java/org/example/misc/ClassHierarchyBuilder.java b/src/main/java/org/example/misc/ClassHierarchyBuilder.java new file mode 100644 index 0000000..e080dfa --- /dev/null +++ b/src/main/java/org/example/misc/ClassHierarchyBuilder.java @@ -0,0 +1,56 @@ +package org.example.misc; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class ClassHierarchyBuilder extends ClassVisitor { + private final Map classToParent = new HashMap<>(); + private final Map depthCache = new HashMap<>(); + + public ClassHierarchyBuilder() { + super(Opcodes.ASM9); + } + + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + // Игнорируем интерфейсы и массивы + if ((access & Opcodes.ACC_INTERFACE) == 0 && superName != null) { + classToParent.put(name, superName); + } + } + + public Map getClassToParent() { + return classToParent; + } + + public int getInheritanceDepth(String className) { + if (depthCache.containsKey(className)) { + return depthCache.get(className); + } + + int depth = calculateDepth(className, new HashSet<>()); + depthCache.put(className, depth); + return depth; + } + + private int calculateDepth(String className, Set visited) { + if (visited.contains(className)) { + return 0; + } + visited.add(className); + + String parent = classToParent.get(className); + + if (parent == null || parent.equals("java/lang/Object")) { + return 0; + } + return 1 + calculateDepth(parent, visited); + } +} + diff --git a/src/main/java/org/example/misc/ConfigReader.java b/src/main/java/org/example/misc/ConfigReader.java new file mode 100644 index 0000000..c3f128f --- /dev/null +++ b/src/main/java/org/example/misc/ConfigReader.java @@ -0,0 +1,35 @@ +package org.example.misc; + +import com.google.gson.Gson; +import org.example.consoler.Config; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class ConfigReader { + private static final String DEFAULT_CONFIG_PATH = "config.json"; + private static final Gson gson = new Gson(); + + public static Config readConfig() throws IOException { + return readConfig(DEFAULT_CONFIG_PATH); + } + + public static Config readConfig(String configPath) throws IOException { + Path path = Paths.get(configPath); + if (!Files.exists(path)) { + throw new IOException("Config file not found: " + configPath); + } + + String content = Files.readString(path); + Config config = gson.fromJson(content, Config.class); + + if (config.getInputJar() == null || config.getOutputJson() == null) { + throw new IOException("Config must contain 'inputJar' and 'outputJson' fields"); + } + + return config; + } +} + diff --git a/src/main/java/org/example/misc/JsonMarshaler.java b/src/main/java/org/example/misc/JsonMarshaler.java new file mode 100644 index 0000000..132b8b1 --- /dev/null +++ b/src/main/java/org/example/misc/JsonMarshaler.java @@ -0,0 +1,21 @@ +package org.example.misc; + +import org.example.consoler.Resulter; + +import java.io.FileWriter; +import java.io.IOException; + +public class JsonMarshaler { + public static void write(Resulter metrics, String outputPath) throws IOException { + try (FileWriter writer = new FileWriter(outputPath)) { + writer.write("{\n"); + writer.write(" \"Max Depth\": " + metrics.getMaxDepth() + ",\n"); + writer.write(" \"Average Depth\": " + String.format("%.2f", metrics.getAvgDepth()) + ",\n"); + writer.write(" \"Average ABC Metric\": " + String.format("%.2f", metrics.getAvgABCMetric()) + ",\n"); + writer.write(" \"Average Overridden Methods\": " + String.format("%.2f", metrics.getAvgOverriddenMethods()) + ",\n"); + writer.write(" \"Average Fields per Class\": " + String.format("%.2f", metrics.getAvgFieldsPerClass()) + "\n"); + writer.write("}\n"); + } + } +} + diff --git "a/src/main/java/org/example/visitor\321\213/ABCMethodVisitor.java" "b/src/main/java/org/example/visitor\321\213/ABCMethodVisitor.java" new file mode 100644 index 0000000..b31c4de --- /dev/null +++ "b/src/main/java/org/example/visitor\321\213/ABCMethodVisitor.java" @@ -0,0 +1,117 @@ +package org.example.visitorы; + +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.util.List; +import java.util.Map; + +/** + * {@link MethodVisitor}, который подсчитывает компоненты ABC-метрики + * (assignments, branches, conditions) для одного метода. + */ +class ABCMethodVisitor extends MethodVisitor { + private final String className; + private final Map> classABCMetrics; + private int assignments = 0; + private int branches = 0; + private int conditions = 0; + + /** + * Создает визитор для одного метода. + * + * @param api версия ASM API + * @param className имя класса-владельца метода + * @param classABCMetrics накопитель ABC-метрик по классам + */ + public ABCMethodVisitor(int api, String className, Map> classABCMetrics) { + super(api); + this.className = className; + this.classABCMetrics = classABCMetrics; + } + + /** + * Обрабатывает инструкции работы с локальными переменными и учитывает сохранения (store). + */ + @Override + public void visitVarInsn(int opcode, int var) { + if (opcode == Opcodes.ISTORE || opcode == Opcodes.LSTORE || + opcode == Opcodes.FSTORE || opcode == Opcodes.DSTORE || + opcode == Opcodes.ASTORE) { + assignments++; + } + } + + /** + * Обрабатывает инструкцию инкремента локальной переменной как присваивание. + */ + @Override + public void visitIincInsn(int var, int increment) { + assignments++; + } + + /** + * Обрабатывает условные и безусловные переходы, разделяя их на ветвления и условия. + */ + @Override + public void visitJumpInsn(int opcode, Label label) { + if (opcode == Opcodes.GOTO || opcode == Opcodes.JSR) { + branches++; + } + else if (opcode == Opcodes.IFEQ || opcode == Opcodes.IFNE || + opcode == Opcodes.IFLT || opcode == Opcodes.IFGE || + opcode == Opcodes.IFGT || opcode == Opcodes.IFLE || + opcode == Opcodes.IF_ICMPEQ || opcode == Opcodes.IF_ICMPNE || + opcode == Opcodes.IF_ICMPLT || opcode == Opcodes.IF_ICMPGE || + opcode == Opcodes.IF_ICMPGT || opcode == Opcodes.IF_ICMPLE || + opcode == Opcodes.IF_ACMPEQ || opcode == Opcodes.IF_ACMPNE || + opcode == Opcodes.IFNULL || opcode == Opcodes.IFNONNULL) { + conditions++; + } + } + + /** + * Обрабатывает табличный switch как одну ветку и одно условие. + */ + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { + branches++; + conditions++; + } + + /** + * Обрабатывает switch с произвольными ключами как одну ветку и одно условие. + */ + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + branches++; + conditions++; + } + + /** + * Обрабатывает простые инструкции, в частности {@code RET} как ветвление. + */ + @Override + public void visitInsn(int opcode) { + if (opcode == Opcodes.RET) { + branches++; + } + } + + /** + * В конце метода вычисляет итоговую ABC-метрику и сохраняет её для соответствующего класса. + */ + @Override + public void visitEnd() { + // sqrt(A^2 + B^2 + C^2) + double abc = Math.sqrt(assignments * assignments + branches * branches + conditions * conditions); + + List abcList = classABCMetrics.get(className); + if (abcList != null) { + abcList.add(abc); + } + } +} + + diff --git "a/src/main/java/org/example/visitor\321\213/ABCMetricVisitor.java" "b/src/main/java/org/example/visitor\321\213/ABCMetricVisitor.java" new file mode 100644 index 0000000..06b7d31 --- /dev/null +++ "b/src/main/java/org/example/visitor\321\213/ABCMetricVisitor.java" @@ -0,0 +1,76 @@ +package org.example.visitorы; + +import org.example.ClassMetrics; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.util.HashMap; +import java.util.Map; + +/** + * {@link ClassVisitor}, который вычисляет ABC-метрику для методов классов + * (Assignments, Branches, Conditions) и агрегирует её по классам. + */ +public class ABCMetricVisitor extends ClassVisitor { + private final Map metricsMap; + private String currentClassName; + private final Map> classABCMetrics = new HashMap<>(); + + public ABCMetricVisitor() { + super(Opcodes.ASM9); + this.metricsMap = new HashMap<>(); + } + + /** + * Вызывается для каждого посещаемого класса. + * Пропускает интерфейсы и инициализирует накопители метрик. + */ + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + currentClassName = name; + metricsMap.computeIfAbsent(name, ClassMetrics::new); + classABCMetrics.put(name, new java.util.ArrayList<>()); + } + + /** + * Вызывается для каждого метода класса. + * Возвращает {@link MethodVisitor}, который считает ABC-метрику по инструкциям метода. + */ + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + // Игнорируем автогенерируемые методы и конструкторы классов + if ((access & Opcodes.ACC_SYNTHETIC) != 0 || + name.equals("") || name.equals("")) { + return null; + } + + return new ABCMethodVisitor(api, currentClassName, classABCMetrics); + } + + /** + * Завершает обработку текущего класса и вычисляет среднее значение ABC-метрики по его методам. + */ + @Override + public void visitEnd() { + ClassMetrics metrics = metricsMap.get(currentClassName); + if (metrics != null) { + java.util.List abcList = classABCMetrics.get(currentClassName); + if (abcList != null && !abcList.isEmpty()) { + double sum = 0.0; + for (Double abc : abcList) { + sum += abc; + } + metrics.setAbcMetric(sum / abcList.size()); + } else { + metrics.setAbcMetric(0.0); + } + } + } + + public Map getMetricsMap() { + return metricsMap; + } +} diff --git a/src/main/java/org/example/visitor/ClassPrinter.java "b/src/main/java/org/example/visitor\321\213/ClassPrinter.java" similarity index 63% rename from src/main/java/org/example/visitor/ClassPrinter.java rename to "src/main/java/org/example/visitor\321\213/ClassPrinter.java" index ba1ab9f..3557abc 100644 --- a/src/main/java/org/example/visitor/ClassPrinter.java +++ "b/src/main/java/org/example/visitor\321\213/ClassPrinter.java" @@ -1,14 +1,24 @@ -package org.example.visitor; +package org.example.visitorы; import org.objectweb.asm.*; import static org.objectweb.asm.Opcodes.ASM8; +/** + * Простой {@link ClassVisitor}, который печатает структуру класса: + * имя, поля и методы. Используется для отладки и демонстрации работы ASM. + */ 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 + " {"); } @@ -29,16 +39,25 @@ public void visitAttribute(Attribute attr) { public void visitInnerClass(String name, String outerName, String innerName, int access) { } + /** + * Печатает объявление поля. + */ public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { System.out.println(" " + desc + " " + name); return null; } + /** + * Печатает сигнатуру метода. + */ public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { System.out.println(" " + name + desc); return null; } + /** + * Закрывает вывод класса фигурной скобкой. + */ public void visitEnd() { System.out.println("}"); } diff --git "a/src/main/java/org/example/visitor\321\213/DepthVisitor.java" "b/src/main/java/org/example/visitor\321\213/DepthVisitor.java" new file mode 100644 index 0000000..14cebe1 --- /dev/null +++ "b/src/main/java/org/example/visitor\321\213/DepthVisitor.java" @@ -0,0 +1,40 @@ +package org.example.visitorы; + +import org.example.ClassMetrics; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; + +import java.util.Map; + +/** + * {@link ClassVisitor}, который готовит метрики для расчета глубины наследования. + * Сама глубина вычисляется позже в {@link MultiVisitor#finalizeDepths()}. + */ +public class DepthVisitor extends ClassVisitor { + private final Map metricsMap; + + /** + * Создает визитор для подготовки метрик глубины наследования. + */ + public DepthVisitor() { + super(Opcodes.ASM9); + this.metricsMap = new java.util.HashMap<>(); + } + + /** + * Создает запись метрик для класса. Глубина наследования будет вычислена позже. + */ + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + metricsMap.computeIfAbsent(name, ClassMetrics::new); + } + + /** + * Возвращает карту метрик по классам. + */ + public Map getMetricsMap() { + return metricsMap; + } +} + diff --git "a/src/main/java/org/example/visitor\321\213/FieldsCounterVisitor.java" "b/src/main/java/org/example/visitor\321\213/FieldsCounterVisitor.java" new file mode 100644 index 0000000..58de5fb --- /dev/null +++ "b/src/main/java/org/example/visitor\321\213/FieldsCounterVisitor.java" @@ -0,0 +1,72 @@ +package org.example.visitorы; + +import org.example.ClassMetrics; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Opcodes; + +import java.util.HashMap; +import java.util.Map; + +/** + * {@link ClassVisitor}, который считает количество полей в каждом классе, + * игнорируя синтетические поля. + */ +public class FieldsCounterVisitor extends ClassVisitor { + private final Map metricsMap; + private String currentClassName; + private int fieldCount = 0; + + /** + * Создает визитор подсчета полей. + */ + public FieldsCounterVisitor() { + super(Opcodes.ASM9); + this.metricsMap = new HashMap<>(); + } + + /** + * Инициализирует счетчики для очередного класса. + * Интерфейсы не учитываются. + */ + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + currentClassName = name; + fieldCount = 0; + metricsMap.computeIfAbsent(name, ClassMetrics::new); + } + + /** + * Обрабатывает объявление поля и увеличивает счетчик + * для несинтетических полей. + */ + @Override + public FieldVisitor visitField(int access, String name, String descriptor, + String signature, Object value) { + // Игнорируем автогенерируемые поля (например, для внутренних классов) + if ((access & Opcodes.ACC_SYNTHETIC) == 0) { + fieldCount++; + } + return null; + } + + /** + * Сохраняет итоговое количество полей в {@link ClassMetrics} для класса. + */ + @Override + public void visitEnd() { + ClassMetrics metrics = metricsMap.get(currentClassName); + if (metrics != null) { + metrics.setFieldsCount(fieldCount); + } + } + + /** + * Возвращает карту метрик по классам. + */ + public Map getMetricsMap() { + return metricsMap; + } +} + diff --git "a/src/main/java/org/example/visitor\321\213/MultiVisitor.java" "b/src/main/java/org/example/visitor\321\213/MultiVisitor.java" new file mode 100644 index 0000000..f2a72c0 --- /dev/null +++ "b/src/main/java/org/example/visitor\321\213/MultiVisitor.java" @@ -0,0 +1,183 @@ +package org.example.visitorы; + +import org.example.ClassMetrics; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Композитный {@link ClassVisitor}, который собирает все метрики за один проход: + * глубину наследования, ABC-метрику, переопределенные методы и количество полей. + */ +public class MultiVisitor extends ClassVisitor { + private final Map classToParent = new HashMap<>(); + private final Map depthCache = new HashMap<>(); + + private final Map metricsMap = new HashMap<>(); + private String currentClassName; + private final Map> classABCMetrics = new HashMap<>(); + private final Map> classMethods = new HashMap<>(); + private Set currentMethods; + private int currentFieldCount; + + public MultiVisitor() { + super(Opcodes.ASM9); + } + + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + // Игнорируем интерфейсы + if ((access & Opcodes.ACC_INTERFACE) != 0) { + return; + } + + currentClassName = name; + + // Строим иерархию классов + if (superName != null) { + classToParent.put(name, superName); + } + + metricsMap.computeIfAbsent(name, ClassMetrics::new); + classABCMetrics.put(name, new ArrayList<>()); + currentMethods = new HashSet<>(); + currentFieldCount = 0; + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, + String signature, Object value) { + // Считаем только несинтетические поля + if ((access & Opcodes.ACC_SYNTHETIC) == 0) { + currentFieldCount++; + } + return null; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + // Игнорируем синтетические методы и конструкторы + boolean isABCMethod = (access & Opcodes.ACC_SYNTHETIC) == 0 && + !name.equals("") && !name.equals(""); + + // Игнорируем синтетические, статические методы и конструкторы + boolean isOverriddenMethod = (access & Opcodes.ACC_SYNTHETIC) == 0 && + (access & Opcodes.ACC_STATIC) == 0 && + !name.equals("") && !name.equals(""); + + if (isOverriddenMethod) { + String methodSignature = name + descriptor; + currentMethods.add(methodSignature); + } + + if (isABCMethod) { + return new ABCMethodVisitor(api, currentClassName, classABCMetrics); + } + + return null; + } + + @Override + public void visitEnd() { + ClassMetrics metrics = metricsMap.get(currentClassName); + if (metrics != null) { + metrics.setFieldsCount(currentFieldCount); + } + + classMethods.put(currentClassName, currentMethods); + + List abcList = classABCMetrics.get(currentClassName); + if (metrics != null && abcList != null && !abcList.isEmpty()) { + double sum = 0.0; + for (Double abc : abcList) { + sum += abc; + } + metrics.setAbcMetric(sum / abcList.size()); + } else if (metrics != null) { + metrics.setAbcMetric(0.0); + } + + if (metrics != null) { + int overriddenCount = countOverriddenMethods(currentClassName, currentMethods); + metrics.setOverriddenMethodsCount(overriddenCount); + } + } + + /** + * Считает количество методов из {@code methods}, которые уже существуют + * в родительском классе. + */ + private int countOverriddenMethods(String className, Set methods) { + int count = 0; + String parent = classToParent.get(className); + + if (parent == null || parent.equals("java/lang/Object")) { + return 0; + } + + Set parentMethods = classMethods.get(parent); + if (parentMethods != null) { + for (String method : methods) { + if (parentMethods.contains(method)) { + count++; + } + } + } + + return count; + } + + /** + * Вычисляет глубины наследования для всех классов на основе построенной иерархии. + */ + private void finalizeDepths() { + for (String className : metricsMap.keySet()) { + ClassMetrics metrics = metricsMap.get(className); + int depth = getInheritanceDepth(className); + metrics.setDepth(depth); + } + } + + private int getInheritanceDepth(String className) { + if (depthCache.containsKey(className)) { + return depthCache.get(className); + } + + int depth = calculateDepth(className, new HashSet<>()); + depthCache.put(className, depth); + return depth; + } + + private int calculateDepth(String className, Set visited) { + if (visited.contains(className)) { + return 0; + } + visited.add(className); + + String parent = classToParent.get(className); + + if (parent == null || parent.equals("java/lang/Object")) { + return 0; + } + return 1 + calculateDepth(parent, visited); + } + + /** + * Объединяет результаты всех метрик в единый список по классам. + * Перед объединением вычисляет глубины наследования. + */ + public List getClassMetricsList() { + finalizeDepths(); + return new ArrayList<>(metricsMap.values()); + } +} diff --git "a/src/main/java/org/example/visitor\321\213/OverriddenMethodsVisitor.java" "b/src/main/java/org/example/visitor\321\213/OverriddenMethodsVisitor.java" new file mode 100644 index 0000000..bc1d96a --- /dev/null +++ "b/src/main/java/org/example/visitor\321\213/OverriddenMethodsVisitor.java" @@ -0,0 +1,111 @@ +package org.example.visitorы; + +import org.example.ClassMetrics; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +/** + * {@link ClassVisitor}, который определяет количество методов, + * переопределяющих методы родительских классов. + */ +public class OverriddenMethodsVisitor extends ClassVisitor { + private final Supplier> classToParentSupplier; + private final Map metricsMap; + private final Map> classMethods = new HashMap<>(); + private String currentClassName; + private java.util.Set currentMethods = new java.util.HashSet<>(); + + /** + * Создает визитор подсчета переопределенных методов. + * + * @param classToParentSupplier поставщик карты иерархии классов (класс -> родитель) + */ + public OverriddenMethodsVisitor(Supplier> classToParentSupplier) { + super(Opcodes.ASM9); + this.classToParentSupplier = classToParentSupplier; + this.metricsMap = new HashMap<>(); + } + + /** + * Подготавливает коллекции для обработки нового класса. + * Интерфейсы игнорируются. + */ + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + currentClassName = name; + currentMethods = new java.util.HashSet<>(); + metricsMap.computeIfAbsent(name, ClassMetrics::new); + } + + /** + * Фиксирует сигнатуры пользовательских методов (не синтетических, + * не конструкторов, не статических) для дальнейшего сравнения с родителями. + */ + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + // Игнорируем автогенерируемые методы, конструкторы и статические методы + if ((access & Opcodes.ACC_SYNTHETIC) != 0 || + (access & Opcodes.ACC_STATIC) != 0 || + name.equals("") || name.equals("")) { + return null; + } + + String methodSignature = name + descriptor; + currentMethods.add(methodSignature); + + return null; + } + + /** + * После обхода всех методов класса вычисляет количество переопределенных + * методов и сохраняет результат в {@link ClassMetrics}. + */ + @Override + public void visitEnd() { + classMethods.put(currentClassName, currentMethods); + int overriddenCount = countOverriddenMethods(currentClassName, currentMethods); + + ClassMetrics metrics = metricsMap.get(currentClassName); + if (metrics != null) { + metrics.setOverriddenMethodsCount(overriddenCount); + } + } + + /** + * Считает количество методов из {@code methods}, которые уже существуют + * в родительском классе. + */ + private int countOverriddenMethods(String className, Set methods) { + int count = 0; + Map classToParent = classToParentSupplier.get(); + String parent = classToParent.get(className); + + if (parent == null || parent.equals("java/lang/Object")) { + return 0; + } + + Set parentMethods = classMethods.get(parent); + if (parentMethods != null) { + for (String method : methods) { + if (parentMethods.contains(method)) { + count++; + } + } + } + + return count; + } + + public Map getMetricsMap() { + return metricsMap; + } +} +