From f2d2313f4ae45c7104e89685833407a8506ca54a Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:18:47 +0000 Subject: [PATCH 1/3] 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 73e39ac64c23b324603d80b1680fd1776e5cea0b Mon Sep 17 00:00:00 2001 From: Vlad Denisov Date: Wed, 3 Dec 2025 23:58:34 +0900 Subject: [PATCH 2/3] feat: add code --- README.md | 16 +- build.gradle.kts | 26 +- gradle.properties | 0 gradlew | 0 settings.gradle.kts | 3 + src/main/java/org/example/Example.java | 29 -- .../java/org/example/example/BubbleSort.java | 29 -- .../org/example/util/ByteCodePrinter.java | 90 ---- .../org/example/util/CheckFrameAnalyzer.java | 475 ------------------ .../org/example/visitor/ClassPrinter.java | 46 -- .../metrics/analysis/HierarchyBuilder.java | 118 +++++ .../metrics/analysis/MetricsAggregator.java | 74 +++ .../metrics/analysis/OverrideResolver.java | 64 +++ .../metrics/classpath/ClasspathLoader.java | 75 +++ src/main/java/org/metrics/cli/Main.java | 114 +++++ src/main/java/org/metrics/io/JarScanner.java | 35 ++ src/main/java/org/metrics/model/ByClass.java | 22 + .../java/org/metrics/model/ClassInfo.java | 29 ++ .../java/org/metrics/model/MethodInfo.java | 19 + src/main/java/org/metrics/model/Report.java | 12 + src/main/java/org/metrics/model/Summary.java | 31 ++ .../org/metrics/output/ConsoleSummary.java | 25 + .../java/org/metrics/output/JsonWriter.java | 28 ++ .../metrics/visit/CollectClassVisitor.java | 65 +++ .../metrics/visit/CollectMethodVisitor.java | 32 ++ .../java/org/metrics/CoreMetricsTest.java | 108 ++++ .../java/org/metrics/JsonSnapshotTest.java | 54 ++ src/test/resources/snapshots/simple.json | 49 ++ 28 files changed, 997 insertions(+), 671 deletions(-) create mode 100644 gradle.properties mode change 100644 => 100755 gradlew delete mode 100644 src/main/java/org/example/Example.java delete mode 100644 src/main/java/org/example/example/BubbleSort.java delete mode 100644 src/main/java/org/example/util/ByteCodePrinter.java delete mode 100644 src/main/java/org/example/util/CheckFrameAnalyzer.java delete mode 100644 src/main/java/org/example/visitor/ClassPrinter.java create mode 100644 src/main/java/org/metrics/analysis/HierarchyBuilder.java create mode 100644 src/main/java/org/metrics/analysis/MetricsAggregator.java create mode 100644 src/main/java/org/metrics/analysis/OverrideResolver.java create mode 100644 src/main/java/org/metrics/classpath/ClasspathLoader.java create mode 100644 src/main/java/org/metrics/cli/Main.java create mode 100644 src/main/java/org/metrics/io/JarScanner.java create mode 100644 src/main/java/org/metrics/model/ByClass.java create mode 100644 src/main/java/org/metrics/model/ClassInfo.java create mode 100644 src/main/java/org/metrics/model/MethodInfo.java create mode 100644 src/main/java/org/metrics/model/Report.java create mode 100644 src/main/java/org/metrics/model/Summary.java create mode 100644 src/main/java/org/metrics/output/ConsoleSummary.java create mode 100644 src/main/java/org/metrics/output/JsonWriter.java create mode 100644 src/main/java/org/metrics/visit/CollectClassVisitor.java create mode 100644 src/main/java/org/metrics/visit/CollectMethodVisitor.java create mode 100644 src/test/java/org/metrics/CoreMetricsTest.java create mode 100644 src/test/java/org/metrics/JsonSnapshotTest.java create mode 100644 src/test/resources/snapshots/simple.json diff --git a/README.md b/README.md index 2486cde..e7d9082 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,18 @@ Гайд по использованию ASM: https://asm.ow2.io/asm4-guide.pdf -Дополнительное (необязательное задание): сделайте агента для сбора покрытия по строчкам \ No newline at end of file +Дополнительное (необязательное задание): сделайте агента для сбора покрытия по строчкам + +## Использование + +Сборка fat JAR: + +``` +./gradlew shadowJar +``` + +Запуск: + +``` +java -jar build/libs/metrics-analyzer.jar --input app.jar --json out.json +``` \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 2b92afd..d9029b3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,20 +1,31 @@ plugins { id("java") + id("application") + id("com.github.johnrengelman.shadow") version "8.1.1" } -group = "org.example" +group = "org.metrics" version = "1.0-SNAPSHOT" repositories { mavenCentral() } +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + 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") + implementation("info.picocli:picocli:4.7.6") + implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2") + testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") @@ -22,4 +33,17 @@ dependencies { tasks.test { useJUnitPlatform() +} + +application { + mainClass.set("org.metrics.cli.Main") +} + +tasks.named("shadowJar") { + archiveBaseName.set("metrics-analyzer") + archiveClassifier.set("") + archiveVersion.set("") + manifest { + attributes(mapOf("Main-Class" to application.mainClass.get())) + } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e69de29 diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/settings.gradle.kts b/settings.gradle.kts index a7f83d7..9a7cba0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,4 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} rootProject.name = "bytecode-template" \ No newline at end of file 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/util/ByteCodePrinter.java b/src/main/java/org/example/util/ByteCodePrinter.java deleted file mode 100644 index 3d1ca38..0000000 --- a/src/main/java/org/example/util/ByteCodePrinter.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.example.util; - -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.TryCatchBlockNode; -import org.objectweb.asm.tree.analysis.*; -import org.objectweb.asm.util.Textifier; -import org.objectweb.asm.util.TraceMethodVisitor; - -import java.io.IOException; -import java.io.PrintWriter; -import java.nio.file.Files; -import java.nio.file.Path; - -public class ByteCodePrinter { - - private static String getUnqualifiedName(final String name) { - var lastSlashIndex = name.lastIndexOf('/'); - if (lastSlashIndex == -1) { - return name; - } else { - int endIndex = name.length(); - if (name.charAt(endIndex - 1) == ';') { - endIndex--; - } - int lastBracketIndex = name.lastIndexOf('['); - if (lastBracketIndex == -1) { - return name.substring(lastSlashIndex + 1, endIndex); - } - return name.substring(0, lastBracketIndex + 1) + name.substring(lastSlashIndex + 1, endIndex); - } - } - private static void analyzeMethod( - final MethodNode method, final Analyzer analyzer, final PrintWriter printWriter) { - var textifier = new Textifier(); - var traceMethodVisitor = new TraceMethodVisitor(textifier); - - printWriter.println(method.name + method.desc); - for (int i = 0; i < method.instructions.size(); ++i) { - method.instructions.get(i).accept(traceMethodVisitor); - - var stringBuilder = new StringBuilder(); - var frame = analyzer.getFrames()[i]; - if (frame == null) { - stringBuilder.append('?'); - } else { - for (int j = 0; j < frame.getLocals(); ++j) { - stringBuilder.append(getUnqualifiedName(frame.getLocal(j).toString())).append(' '); - } - stringBuilder.append(" : "); - for (int j = 0; j < frame.getStackSize(); ++j) { - stringBuilder.append(getUnqualifiedName(frame.getStack(j).toString())).append(' '); - } - } - while (stringBuilder.length() < method.maxStack + method.maxLocals + 1) { - stringBuilder.append(' '); - } - printWriter.print(Integer.toString(i + 100000).substring(1)); - printWriter.print( - " " + stringBuilder + " : " + textifier.text.get(textifier.text.size() - 1)); - } - for (TryCatchBlockNode tryCatchBlock : method.tryCatchBlocks) { - tryCatchBlock.accept(traceMethodVisitor); - printWriter.print(" " + textifier.text.get(textifier.text.size() - 1)); - } - printWriter.println(); - } - - public void printBytecode(ClassNode cn) { - var sortMethod = cn.methods.get(1); - var analyzer = new CheckFrameAnalyzer<>(new BasicVerifier()); - try { - analyzer.analyze("dummy", sortMethod); - } catch (AnalyzerException e) { - throw new RuntimeException(e); - } - var pw = new PrintWriter(System.out); - analyzeMethod(sortMethod, analyzer, pw); - pw.flush(); - } - - 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 classReader = new ClassReader(classFileBytes); - classReader.accept(cn, ClassReader.EXPAND_FRAMES); - printBytecode(cn); - } -} diff --git a/src/main/java/org/example/util/CheckFrameAnalyzer.java b/src/main/java/org/example/util/CheckFrameAnalyzer.java deleted file mode 100644 index 5231ca7..0000000 --- a/src/main/java/org/example/util/CheckFrameAnalyzer.java +++ /dev/null @@ -1,475 +0,0 @@ -package org.example.util; - -// ASM: a very small and fast Java bytecode manipulation framework -// Copyright (c) 2000-2011 INRIA, France Telecom -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions -// are met: -// 1. Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// 3. Neither the name of the copyright holders nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. - -import java.util.Collections; -import java.util.List; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.FrameNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.InsnNode; -import org.objectweb.asm.tree.JumpInsnNode; -import org.objectweb.asm.tree.LabelNode; -import org.objectweb.asm.tree.LookupSwitchInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.TableSwitchInsnNode; -import org.objectweb.asm.tree.TryCatchBlockNode; -import org.objectweb.asm.tree.TypeInsnNode; -import org.objectweb.asm.tree.analysis.Analyzer; -import org.objectweb.asm.tree.analysis.AnalyzerException; -import org.objectweb.asm.tree.analysis.Frame; -import org.objectweb.asm.tree.analysis.Interpreter; -import org.objectweb.asm.tree.analysis.Value; - -/** - * An {@link Analyzer} subclass which checks that methods provide stack map frames where expected - * (i.e. at jump target and after instructions without immediate successor), and that these stack - * map frames are valid (for the provided interpreter; they may still be invalid for the JVM, if the - * {@link Interpreter} uses a simplified type system compared to the JVM verifier). This is done in - * two steps: - * - *
    - *
  • First, the stack map frames in {@link FrameNode}s are expanded, and stored at their - * respective instruction offsets. The expansion process uncompresses the APPEND, CHOP and - * SAME frames to FULL frames. It also converts the stack map frame verification types to - * {@link Value}s, via the provided {@link Interpreter}. The expansion is done in {@link - * #expandFrames}, by looking at each {@link FrameNode} in sequence (compressed frames are - * defined relatively to the previous {@link FrameNode}, or the implicit first frame). The - * actual decompression is done in {@link #expandFrame}, and the type conversion in {@link - * #newFrameValue}. - *
  • Next, the method instructions are checked in sequence. Starting from the implicit initial - * frame, the execution of each instruction i is simulated on the current stack map - * frame, with the {@link Frame#execute} method. This gives a new stack map frame f, - * representing the stack map frame state after the execution of i. Then: - *
      - *
    • If there is a next instruction and if the control flow cannot continue to it (e.g. if - * i is a RETURN or an ATHROW, for instance): an existing stack map frame - * f0 (coming from the first step) is expected after i. - *
    • If there is a next instruction and if the control flow can continue to it (e.g. if - * i is a ALOAD, for instance): either there an existing stack map frame - * f0 (coming from the first step) after i, or there is none. In the - * first case f and f0 must be compatible: the types in - * f must be sub types of the corresponding types in the existing frame - * f0 (otherwise an exception is thrown). In the second case, f0 is - * simply set to the value of f. - *
    • If the control flow can continue to some instruction j (e.g. if i - * is an IF_EQ, for instance): an existing stack map frame f0 (coming from the - * first step) is expected at j, which must be compatible with f (as - * defined previously). - *
    - * The sequential loop over the instructions is done in {@link #init}, which is called from - * the {@link Analyzer#analyze} method. Cases where the control flow cannot continue to the - * next instruction are handled in {@link #endControlFlow}. Cases where the control flow can - * continue to the next instruction, or jump to another instruction, are handled in {@link - * #checkFrame}. This method checks that an existing stack map frame is present when required, - * and checks the stack map frames compatibility with {@link #checkMerge}. - *
- * - * @author Eric Bruneton - * @param type of the {@link Value} used for the analysis. - */ -class CheckFrameAnalyzer extends Analyzer { - - /** The interpreter to use to symbolically interpret the bytecode instructions. */ - private final Interpreter interpreter; - - /** The instructions of the currently analyzed method. */ - private InsnList insnList; - - /** - * double values are represented with two elements. - */ - private int currentLocals; - - CheckFrameAnalyzer(final Interpreter interpreter) { - super(interpreter); - this.interpreter = interpreter; - } - - @Override - protected void init(final String owner, final MethodNode method) throws AnalyzerException { - insnList = method.instructions; - currentLocals = Type.getArgumentsAndReturnSizes(method.desc) >> 2; - - Frame[] frames = getFrames(); - Frame currentFrame = frames[0]; - expandFrames(owner, method, currentFrame); - for (int insnIndex = 0; insnIndex < insnList.size(); ++insnIndex) { - Frame oldFrame = frames[insnIndex]; - - // Simulate the execution of this instruction. - AbstractInsnNode insnNode = null; - try { - insnNode = method.instructions.get(insnIndex); - int insnOpcode = insnNode.getOpcode(); - int insnType = insnNode.getType(); - - if (insnType == AbstractInsnNode.LABEL - || insnType == AbstractInsnNode.LINE - || insnType == AbstractInsnNode.FRAME) { - checkFrame(insnIndex + 1, oldFrame, /* requireFrame = */ false); - } else { - currentFrame.init(oldFrame).execute(insnNode, interpreter); - - if (insnNode instanceof JumpInsnNode) { - if (insnOpcode == JSR) { - throw new AnalyzerException(insnNode, "JSR instructions are unsupported"); - } - JumpInsnNode jumpInsn = (JumpInsnNode) insnNode; - int targetInsnIndex = insnList.indexOf(jumpInsn.label); - checkFrame(targetInsnIndex, currentFrame, /* requireFrame = */ true); - if (insnOpcode == GOTO) { - endControlFlow(insnIndex); - } else { - checkFrame(insnIndex + 1, currentFrame, /* requireFrame = */ false); - } - } else if (insnNode instanceof LookupSwitchInsnNode) { - LookupSwitchInsnNode lookupSwitchInsn = (LookupSwitchInsnNode) insnNode; - int targetInsnIndex = insnList.indexOf(lookupSwitchInsn.dflt); - checkFrame(targetInsnIndex, currentFrame, /* requireFrame = */ true); - for (int i = 0; i < lookupSwitchInsn.labels.size(); ++i) { - LabelNode label = lookupSwitchInsn.labels.get(i); - targetInsnIndex = insnList.indexOf(label); - currentFrame.initJumpTarget(insnOpcode, label); - checkFrame(targetInsnIndex, currentFrame, /* requireFrame = */ true); - } - endControlFlow(insnIndex); - } else if (insnNode instanceof TableSwitchInsnNode) { - TableSwitchInsnNode tableSwitchInsn = (TableSwitchInsnNode) insnNode; - int targetInsnIndex = insnList.indexOf(tableSwitchInsn.dflt); - currentFrame.initJumpTarget(insnOpcode, tableSwitchInsn.dflt); - checkFrame(targetInsnIndex, currentFrame, /* requireFrame = */ true); - newControlFlowEdge(insnIndex, targetInsnIndex); - for (int i = 0; i < tableSwitchInsn.labels.size(); ++i) { - LabelNode label = tableSwitchInsn.labels.get(i); - currentFrame.initJumpTarget(insnOpcode, label); - targetInsnIndex = insnList.indexOf(label); - checkFrame(targetInsnIndex, currentFrame, /* requireFrame = */ true); - } - endControlFlow(insnIndex); - } else if (insnOpcode == RET) { - throw new AnalyzerException(insnNode, "RET instructions are unsupported"); - } else if (insnOpcode != ATHROW && (insnOpcode < IRETURN || insnOpcode > RETURN)) { - checkFrame(insnIndex + 1, currentFrame, /* requireFrame = */ false); - } else { - endControlFlow(insnIndex); - } - } - - List insnHandlers = getHandlers(insnIndex); - if (insnHandlers != null) { - for (TryCatchBlockNode tryCatchBlock : insnHandlers) { - Type catchType; - if (tryCatchBlock.type == null) { - catchType = Type.getObjectType("java/lang/Throwable"); - } else { - catchType = Type.getObjectType(tryCatchBlock.type); - } - Frame handler = newFrame(oldFrame); - handler.clearStack(); - handler.push(interpreter.newExceptionValue(tryCatchBlock, handler, catchType)); - checkFrame(insnList.indexOf(tryCatchBlock.handler), handler, /* requireFrame = */ true); - } - } - - if (!hasNextJvmInsnOrFrame(insnIndex)) { - break; - } - } catch (AnalyzerException e) { - throw new AnalyzerException( - e.node, "Error at instruction " + insnIndex + ": " + e.getMessage(), e); - } catch (RuntimeException e) { - // DontCheck(IllegalCatch): can't be fixed, for backward compatibility. - throw new AnalyzerException( - insnNode, "Error at instruction " + insnIndex + ": " + e.getMessage(), e); - } - } - } - - /** - * Expands the {@link FrameNode} "instructions" of the given method into {@link Frame} objects and - * also associated with the label and line number nodes immediately preceding each frame node. - * - * @param owner the internal name of the class to which 'method' belongs. - * @param method the method whose frames must be expanded. - * @param initialFrame the implicit initial frame of 'method'. - * @throws AnalyzerException if the stack map frames of 'method', i.e. its FrameNode - * "instructions", are invalid. - */ - private void expandFrames( - final String owner, final MethodNode method, final Frame initialFrame) - throws AnalyzerException { - int lastJvmOrFrameInsnIndex = -1; - Frame currentFrame = initialFrame; - int currentInsnIndex = 0; - for (AbstractInsnNode insnNode : method.instructions) { - if (insnNode instanceof FrameNode) { - try { - currentFrame = expandFrame(owner, currentFrame, (FrameNode) insnNode); - } catch (AnalyzerException e) { - throw new AnalyzerException( - e.node, "Error at instruction " + currentInsnIndex + ": " + e.getMessage(), e); - } - for (int index = lastJvmOrFrameInsnIndex + 1; index <= currentInsnIndex; ++index) { - getFrames()[index] = currentFrame; - } - } - if (isJvmInsnNode(insnNode) || insnNode instanceof FrameNode) { - lastJvmOrFrameInsnIndex = currentInsnIndex; - } - currentInsnIndex += 1; - } - } - - /** - * Returns the expanded representation of the given {@link FrameNode}. - * - * @param owner the internal name of the class to which 'frameNode' belongs. - * @param previousFrame the frame before 'frameNode', in expanded form. - * @param frameNode a possibly compressed stack map frame. - * @return the expanded version of 'frameNode'. - * @throws AnalyzerException if 'frameNode' is invalid. - */ - private Frame expandFrame( - final String owner, final Frame previousFrame, final FrameNode frameNode) - throws AnalyzerException { - Frame frame = newFrame(previousFrame); - List locals = frameNode.local == null ? Collections.emptyList() : frameNode.local; - int currentLocal = currentLocals; - switch (frameNode.type) { - case Opcodes.F_NEW: - case Opcodes.F_FULL: - currentLocal = 0; - // fall through - case Opcodes.F_APPEND: - for (Object type : locals) { - V value = newFrameValue(owner, frameNode, type); - if (currentLocal + value.getSize() > frame.getLocals()) { - throw new AnalyzerException(frameNode, "Cannot append more locals than maxLocals"); - } - frame.setLocal(currentLocal++, value); - if (value.getSize() == 2) { - frame.setLocal(currentLocal++, interpreter.newValue(null)); - } - } - break; - case Opcodes.F_CHOP: - for (Object unusedType : locals) { - if (currentLocal <= 0) { - throw new AnalyzerException(frameNode, "Cannot chop more locals than defined"); - } - if (currentLocal > 1 && frame.getLocal(currentLocal - 2).getSize() == 2) { - currentLocal -= 2; - } else { - currentLocal -= 1; - } - } - break; - case Opcodes.F_SAME: - case Opcodes.F_SAME1: - break; - default: - throw new AnalyzerException(frameNode, "Illegal frame type " + frameNode.type); - } - currentLocals = currentLocal; - while (currentLocal < frame.getLocals()) { - frame.setLocal(currentLocal++, interpreter.newValue(null)); - } - - List stack = frameNode.stack == null ? Collections.emptyList() : frameNode.stack; - frame.clearStack(); - for (Object type : stack) { - frame.push(newFrameValue(owner, frameNode, type)); - } - return frame; - } - - /** - * Creates a new {@link Value} that represents the given stack map frame type. - * - * @param owner the internal name of the class to which 'frameNode' belongs. - * @param frameNode the stack map frame to which 'type' belongs. - * @param type an Integer, String or LabelNode object representing a primitive, reference or - * uninitialized a stack map frame type, respectively. See {@link FrameNode}. - * @return a value that represents the given type. - * @throws AnalyzerException if 'type' is an invalid stack map frame type. - */ - private V newFrameValue(final String owner, final FrameNode frameNode, final Object type) - throws AnalyzerException { - if (type == Opcodes.TOP) { - return interpreter.newValue(null); - } else if (type == Opcodes.INTEGER) { - return interpreter.newValue(Type.INT_TYPE); - } else if (type == Opcodes.FLOAT) { - return interpreter.newValue(Type.FLOAT_TYPE); - } else if (type == Opcodes.LONG) { - return interpreter.newValue(Type.LONG_TYPE); - } else if (type == Opcodes.DOUBLE) { - return interpreter.newValue(Type.DOUBLE_TYPE); - } else if (type == Opcodes.NULL) { - return interpreter.newOperation(new InsnNode(Opcodes.ACONST_NULL)); - } else if (type == Opcodes.UNINITIALIZED_THIS) { - return interpreter.newValue(Type.getObjectType(owner)); - } else if (type instanceof String) { - return interpreter.newValue(Type.getObjectType((String) type)); - } else if (type instanceof LabelNode) { - AbstractInsnNode referencedNode = (LabelNode) type; - while (referencedNode != null && !isJvmInsnNode(referencedNode)) { - referencedNode = referencedNode.getNext(); - } - if (referencedNode == null || referencedNode.getOpcode() != Opcodes.NEW) { - throw new AnalyzerException(frameNode, "LabelNode does not designate a NEW instruction"); - } - return interpreter.newValue(Type.getObjectType(((TypeInsnNode) referencedNode).desc)); - } - throw new AnalyzerException(frameNode, "Illegal stack map frame value " + type); - } - - /** - * Checks that the given frame is compatible with the frame at the given instruction index, if - * any. If there is no frame at this instruction index and none is required, the frame at - * 'insnIndex' is set to the given frame. Otherwise, if the merge of the two frames is not equal - * to the current frame at 'insnIndex', an exception is thrown. - * - * @param insnIndex an instruction index. - * @param frame a frame. This frame is left unchanged by this method. - * 'insnIndex'. - * @throws AnalyzerException if the frames have incompatible sizes or if the frame at 'insnIndex' - * is missing (if required) or not compatible with 'frame'. - */ - private void checkFrame(final int insnIndex, final Frame frame, final boolean requireFrame) - throws AnalyzerException { - Frame oldFrame = getFrames()[insnIndex]; - if (oldFrame == null) { - if (requireFrame) { - throw new AnalyzerException(null, "Expected stack map frame at instruction " + insnIndex); - } - getFrames()[insnIndex] = newFrame(frame); - } else { - String error = checkMerge(frame, oldFrame); - if (error != null) { - throw new AnalyzerException( - null, - "Stack map frame incompatible with frame at instruction " - + insnIndex - + " (" - + error - + ")"); - } - } - } - - /** - * Checks that merging the two given frames would not produce any change, i.e. that the types in - * the source frame are sub types of the corresponding types in the destination frame. - * - * @param srcFrame a source frame. This frame is left unchanged by this method. - * @param dstFrame a destination frame. This frame is left unchanged by this method. - * @return an error message if the frames have incompatible sizes, or if a type in the source - * frame is not a sub type of the corresponding type in the destination frame. Returns - * {@literal null} otherwise. - */ - private String checkMerge(final Frame srcFrame, final Frame dstFrame) { - int numLocals = srcFrame.getLocals(); - if (numLocals != dstFrame.getLocals()) { - throw new AssertionError(); - } - for (int i = 0; i < numLocals; ++i) { - V v = interpreter.merge(srcFrame.getLocal(i), dstFrame.getLocal(i)); - if (!v.equals(dstFrame.getLocal(i))) { - return "incompatible types at local " - + i - + ": " - + srcFrame.getLocal(i) - + " and " - + dstFrame.getLocal(i); - } - } - int numStack = srcFrame.getStackSize(); - if (numStack != dstFrame.getStackSize()) { - return "incompatible stack heights"; - } - for (int i = 0; i < numStack; ++i) { - V v = interpreter.merge(srcFrame.getStack(i), dstFrame.getStack(i)); - if (!v.equals(dstFrame.getStack(i))) { - return "incompatible types at stack item " - + i - + ": " - + srcFrame.getStack(i) - + " and " - + dstFrame.getStack(i); - } - } - return null; - } - - /** - * Ends the control flow graph at the given instruction. This method checks that there is an - * existing frame for the next instruction, if any. - * - * @param insnIndex an instruction index. - * @throws AnalyzerException if 'insnIndex' is not the last instruction and there is no frame at - * 'insnIndex' + 1 in {@link #getFrames}. - */ - private void endControlFlow(final int insnIndex) throws AnalyzerException { - if (hasNextJvmInsnOrFrame(insnIndex) && getFrames()[insnIndex + 1] == null) { - throw new AnalyzerException( - null, "Expected stack map frame at instruction " + (insnIndex + 1)); - } - } - - /** - * Returns true if the given instruction is followed by a JVM instruction or a by stack map frame. - * - * @param insnIndex an instruction index. - * @return true if 'insnIndex' is followed by a JVM instruction or a by stack map frame. - */ - private boolean hasNextJvmInsnOrFrame(final int insnIndex) { - AbstractInsnNode insn = insnList.get(insnIndex).getNext(); - while (insn != null) { - if (isJvmInsnNode(insn) || insn instanceof FrameNode) { - return true; - } - insn = insn.getNext(); - } - return false; - } - - /** - * Returns true if the given instruction node corresponds to a real JVM instruction. - * - * @param insnNode an instruction node. - * @return true except for label, line number and stack map frame nodes. - */ - private static boolean isJvmInsnNode(final AbstractInsnNode insnNode) { - return insnNode.getOpcode() >= 0; - } -} 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("}"); - } -} - diff --git a/src/main/java/org/metrics/analysis/HierarchyBuilder.java b/src/main/java/org/metrics/analysis/HierarchyBuilder.java new file mode 100644 index 0000000..8992153 --- /dev/null +++ b/src/main/java/org/metrics/analysis/HierarchyBuilder.java @@ -0,0 +1,118 @@ +package org.metrics.analysis; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.metrics.model.ClassInfo; + +public final class HierarchyBuilder { + private final Map ditCache = new HashMap<>(); + + public void computeAllDit(Map classesByName, boolean failOnMissingSuper) { + for (ClassInfo ci : classesByName.values()) { + if (!ci.isInterface) { + ci.dit = computeDit(ci.name, classesByName, failOnMissingSuper); + } else { + ci.dit = 0; + } + } + } + + public int computeDit(String className, Map classesByName, boolean failOnMissingSuper) { + Integer cached = ditCache.get(className); + if (cached != null) return cached; + ClassInfo ci = classesByName.get(className); + if (ci == null) { + if (failOnMissingSuper) { + throw new IllegalStateException("Class not found for DIT: " + className); + } + ditCache.put(className, 0); + return 0; + } + if (ci.isInterface) { + ditCache.put(className, 0); + return 0; + } + String parent = ci.superName; + if (parent == null || "java/lang/Object".equals(parent)) { + ditCache.put(className, 0); + return 0; + } + if (!classesByName.containsKey(parent)) { + if (failOnMissingSuper) { + throw new IllegalStateException("Missing superclass: " + parent + " for " + ci.name); + } + ditCache.put(className, 0); + return 0; + } + int depth = 1 + computeDit(parent, classesByName, failOnMissingSuper); + ditCache.put(className, depth); + return depth; + } + + public List ancestorsOf(String className, Map classesByName, boolean failOnMissingSuper) { + List result = new ArrayList<>(); + // цепочка суперклассов + String current = className; + while (true) { + ClassInfo ci = classesByName.get(current); + if (ci == null) { + if (failOnMissingSuper) { + throw new IllegalStateException("Class not found: " + current); + } + break; + } + String parent = ci.superName; + if (parent == null || "java/lang/Object".equals(parent)) break; + result.add(parent); + current = parent; + } + // интерфейсы по цепочке (bfs) + Set visited = new HashSet<>(); + Deque queue = new ArrayDeque<>(); + ClassInfo root = classesByName.get(className); + if (root != null) { + for (String itf : root.interfaces) { + if (visited.add(itf)) queue.add(itf); + } + } + // также интерфейсы суперклассов + current = className; + while (true) { + ClassInfo ci = classesByName.get(current); + if (ci == null) break; + String parent = ci.superName; + if (parent == null || "java/lang/Object".equals(parent)) break; + ClassInfo pci = classesByName.get(parent); + if (pci != null) { + for (String itf : pci.interfaces) { + if (visited.add(itf)) queue.add(itf); + } + } else if (failOnMissingSuper) { + throw new IllegalStateException("Missing superclass for interface scan: " + parent); + } else { + break; + } + current = parent; + } + while (!queue.isEmpty()) { + String itf = queue.removeFirst(); + result.add(itf); + ClassInfo ici = classesByName.get(itf); + if (ici != null) { + for (String parentItf : ici.interfaces) { + if (visited.add(parentItf)) queue.addLast(parentItf); + } + } + } + return result; + } +} + + diff --git a/src/main/java/org/metrics/analysis/MetricsAggregator.java b/src/main/java/org/metrics/analysis/MetricsAggregator.java new file mode 100644 index 0000000..91eff55 --- /dev/null +++ b/src/main/java/org/metrics/analysis/MetricsAggregator.java @@ -0,0 +1,74 @@ +package org.metrics.analysis; + +import org.metrics.model.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public final class MetricsAggregator { + public Report aggregate(String projectName, Map classesByName) { + Report report = new Report(); + report.project = projectName; + + Summary summary = report.summary; + Collection all = classesByName.values(); + int classesCount = 0; + int interfacesCount = 0; + int maxDit = 0; + long ditSum = 0L; + long totalAbcStores = 0L; + long totalMethods = 0L; + long totalOverridden = 0L; + long totalFields = 0L; + + List byClassList = new ArrayList<>(); + for (ClassInfo ci : all) { + if (ci.isInterface) { + interfacesCount++; + } else { + classesCount++; + maxDit = Math.max(maxDit, ci.dit); + ditSum += ci.dit; + totalFields += ci.fieldsCount; + } + int classMethodStores = 0; + int declared = ci.methods.size(); + int overridden = ci.overriddenCount; + for (MethodInfo mi : ci.methods) { + classMethodStores += mi.abcStores; + } + ci.abcStoresTotal = classMethodStores; + totalAbcStores += classMethodStores; + totalMethods += declared; + totalOverridden += overridden; + + ByClass bc = new ByClass(); + bc.name = ci.name; + bc.isInterface = ci.isInterface; + bc.superName = ci.superName; + bc.interfaces = ci.interfaces; + bc.dit = ci.dit; + bc.fieldsCount = ci.fieldsCount; + bc.methods.declared = declared; + bc.methods.overridden = overridden; + bc.methods.abcStores = classMethodStores; + byClassList.add(bc); + } + + summary.classesAnalyzed = classesCount; + summary.interfacesAnalyzed = interfacesCount; + summary.inheritanceDepth.max = maxDit; + summary.inheritanceDepth.avg = classesCount == 0 ? 0.0 : (double) ditSum / classesCount; + summary.abc.totalStores = (int) totalAbcStores; + summary.abc.avgStoresPerClass = classesCount == 0 ? 0.0 : (double) totalAbcStores / classesCount; + summary.abc.avgStoresPerMethod = totalMethods == 0 ? 0.0 : (double) totalAbcStores / totalMethods; + summary.overrides.avgOverriddenMethodsPerClass = classesCount == 0 ? 0.0 : (double) totalOverridden / classesCount; + summary.fields.avgFieldsPerClass = classesCount == 0 ? 0.0 : (double) totalFields / classesCount; + report.byClass = byClassList; + return report; + } +} + + diff --git a/src/main/java/org/metrics/analysis/OverrideResolver.java b/src/main/java/org/metrics/analysis/OverrideResolver.java new file mode 100644 index 0000000..b741e45 --- /dev/null +++ b/src/main/java/org/metrics/analysis/OverrideResolver.java @@ -0,0 +1,64 @@ +package org.metrics.analysis; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.metrics.model.ClassInfo; +import org.metrics.model.MethodInfo; +import org.objectweb.asm.Opcodes; + +public final class OverrideResolver { + private final HierarchyBuilder hierarchyBuilder = new HierarchyBuilder(); + + public void resolve(Map classesByName, boolean failOnMissingSuper) { + Objects.requireNonNull(classesByName, "classesByName"); + // индекс методов по классам + Map> methodsByClassAndSig = new HashMap<>(); + for (ClassInfo ci : classesByName.values()) { + Map bySig = new HashMap<>(); + for (MethodInfo mi : ci.methods) { + bySig.put(signatureOf(mi), mi); + } + methodsByClassAndSig.put(ci.name, bySig); + } + + for (ClassInfo ci : classesByName.values()) { + if (ci.isInterface) continue; + int overridden = 0; + List ancestors = hierarchyBuilder.ancestorsOf(ci.name, classesByName, failOnMissingSuper); + for (MethodInfo mi : ci.methods) { + if (isSkippable(mi)) continue; + String sig = signatureOf(mi); + boolean found = false; + for (String anc : ancestors) { + Map ancMethods = methodsByClassAndSig.get(anc); + if (ancMethods != null && ancMethods.containsKey(sig)) { + found = true; + break; + } + } + if (found) { + mi.overrides = true; + overridden++; + } + } + ci.overriddenCount = overridden; + } + } + + private static boolean isSkippable(MethodInfo mi) { + if ("".equals(mi.name) || "".equals(mi.name)) return true; + int acc = mi.access; + if ((acc & Opcodes.ACC_PRIVATE) != 0) return true; + if ((acc & Opcodes.ACC_STATIC) != 0) return true; + return false; + } + + private static String signatureOf(MethodInfo mi) { + return mi.name + mi.descriptor; + } +} + + diff --git a/src/main/java/org/metrics/classpath/ClasspathLoader.java b/src/main/java/org/metrics/classpath/ClasspathLoader.java new file mode 100644 index 0000000..e8d4ac9 --- /dev/null +++ b/src/main/java/org/metrics/classpath/ClasspathLoader.java @@ -0,0 +1,75 @@ +package org.metrics.classpath; + +import org.metrics.model.ClassInfo; +import org.metrics.visit.CollectClassVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +public final class ClasspathLoader implements AutoCloseable { + private final URLClassLoader loader; + + public ClasspathLoader(String classpath) throws IOException { + this.loader = new URLClassLoader(toUrls(classpath), ClassLoader.getSystemClassLoader()); + } + + public ClassInfo load(String internalName) throws IOException { + Objects.requireNonNull(internalName, "internalName"); + String resource = internalName + ".class"; + try (InputStream in = loader.getResourceAsStream(resource)) { + if (in == null) { + return null; + } + final ClassInfo[] holder = new ClassInfo[1]; + Consumer sink = ci -> holder[0] = ci; + new ClassReader(in).accept(new CollectClassVisitor(Opcodes.ASM9, sink), ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + return holder[0]; + } + } + + private static URL[] toUrls(String classpath) throws IOException { + if (classpath == null || classpath.isBlank()) return new URL[0]; + String[] parts = classpath.split(File.pathSeparator); + List urls = new ArrayList<>(); + for (String p : parts) { + if (p.endsWith("/*")) { + Path dir = Path.of(p.substring(0, p.length() - 2)); + if (Files.isDirectory(dir)) { + try (var stream = Files.list(dir)) { + stream.filter(f -> f.toString().endsWith(".jar")) + .forEach(j -> { + try { + urls.add(j.toUri().toURL()); + } catch (Exception ignored) { + } + }); + } + } + } else { + Path path = Path.of(p); + if (Files.isDirectory(path) || p.endsWith(".jar")) { + urls.add(path.toUri().toURL()); + } + } + } + return urls.toArray(URL[]::new); + } + + @Override + public void close() throws IOException { + loader.close(); + } +} + + diff --git a/src/main/java/org/metrics/cli/Main.java b/src/main/java/org/metrics/cli/Main.java new file mode 100644 index 0000000..16c03ec --- /dev/null +++ b/src/main/java/org/metrics/cli/Main.java @@ -0,0 +1,114 @@ +package org.metrics.cli; + +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; + +import org.metrics.analysis.HierarchyBuilder; +import org.metrics.analysis.MetricsAggregator; +import org.metrics.analysis.OverrideResolver; +import org.metrics.classpath.ClasspathLoader; +import org.metrics.io.JarScanner; +import org.metrics.model.ClassInfo; +import org.metrics.model.Report; +import org.metrics.output.ConsoleSummary; +import org.metrics.output.JsonWriter; +import org.metrics.visit.CollectClassVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command( + name = "metrics-analyzer", + mixinStandardHelpOptions = true, + version = "metrics-analyzer 0.1" +) +public final class Main implements Callable { + + @Option(names = {"-i", "--input"}, required = true, description = "Input JAR to analyze") + private Path inputJar; + + @Option(names = {"-o", "--json"}, description = "Path to write JSON report") + private Path jsonOutput; + + @Option(names = {"-cp", "--classpath"}, description = "Additional classpath (e.g. \"lib/*:rt.jar\")") + private String classpath; + + @Option(names = {"--fail-on-missing-super"}, description = "Fail if a superclass/interface cannot be resolved") + private boolean failOnMissingSuper; + + @Override + public Integer call() { + try { + // сканируем jar и собираем классы + Map classesByName = new HashMap<>(); + JarScanner scanner = new JarScanner(); + scanner.scan(inputJar, (ClassReader reader) -> { + final ClassInfo[] holder = new ClassInfo[1]; + var sink = (java.util.function.Consumer) (ci) -> holder[0] = ci; + reader.accept(new CollectClassVisitor(Opcodes.ASM9, sink), ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + ClassInfo ci = holder[0]; + if (ci != null) { + classesByName.put(ci.name, ci); + } + }); + + // загружаем внешних предков из classpath + if (classpath != null && !classpath.isBlank()) { + try (ClasspathLoader loader = new ClasspathLoader(classpath)) { + Deque queue = new ArrayDeque<>(); + Set seen = new HashSet<>(classesByName.keySet()); + for (ClassInfo ci : classesByName.values()) { + if (ci.superName != null) queue.add(ci.superName); + for (String itf : ci.interfaces) queue.add(itf); + } + while (!queue.isEmpty()) { + String name = queue.removeFirst(); + if (name == null || "java/lang/Object".equals(name)) continue; + if (!seen.add(name)) continue; + ClassInfo ext = loader.load(name); + if (ext != null) { + classesByName.put(ext.name, ext); + if (ext.superName != null) queue.add(ext.superName); + for (String itf : ext.interfaces) queue.add(itf); + } else if (failOnMissingSuper) { + throw new IllegalStateException("Missing class on classpath: " + name); + } + } + } + } + + // считаем dit и переопределения + HierarchyBuilder hierarchy = new HierarchyBuilder(); + hierarchy.computeAllDit(classesByName, failOnMissingSuper); + OverrideResolver overrides = new OverrideResolver(); + overrides.resolve(classesByName, failOnMissingSuper); + + MetricsAggregator aggregator = new MetricsAggregator(); + Report report = aggregator.aggregate(inputJar.getFileName().toString(), classesByName); + + new ConsoleSummary().print(report); + if (jsonOutput != null) { + new JsonWriter().write(report, jsonOutput); + } + return 0; + } catch (Exception e) { + System.err.println("Error: " + e.getMessage()); + return 1; + } + } + + public static void main(String[] args) { + int exit = new CommandLine(new Main()).execute(args); + System.exit(exit); + } +} + diff --git a/src/main/java/org/metrics/io/JarScanner.java b/src/main/java/org/metrics/io/JarScanner.java new file mode 100644 index 0000000..da4d46a --- /dev/null +++ b/src/main/java/org/metrics/io/JarScanner.java @@ -0,0 +1,35 @@ +package org.metrics.io; + +import org.objectweb.asm.ClassReader; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Objects; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public final class JarScanner { + @FunctionalInterface + public interface ReaderHandler { + void accept(ClassReader reader) throws IOException; + } + + public void scan(Path jarPath, ReaderHandler handler) throws IOException { + Objects.requireNonNull(jarPath, "jarPath"); + Objects.requireNonNull(handler, "handler"); + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + var entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.isDirectory()) continue; + if (!entry.getName().endsWith(".class")) continue; + try (InputStream in = jarFile.getInputStream(entry)) { + ClassReader reader = new ClassReader(in); + handler.accept(reader); + } + } + } + } +} + diff --git a/src/main/java/org/metrics/model/ByClass.java b/src/main/java/org/metrics/model/ByClass.java new file mode 100644 index 0000000..c109644 --- /dev/null +++ b/src/main/java/org/metrics/model/ByClass.java @@ -0,0 +1,22 @@ +package org.metrics.model; + +import java.util.ArrayList; +import java.util.List; + +public final class ByClass { + public String name; + public boolean isInterface; + public String superName; + public List interfaces = new ArrayList<>(); + public int dit; + public int fieldsCount; + public MethodsSummary methods = new MethodsSummary(); + + public static final class MethodsSummary { + public int declared; + public int overridden; + public int abcStores; + } +} + + diff --git a/src/main/java/org/metrics/model/ClassInfo.java b/src/main/java/org/metrics/model/ClassInfo.java new file mode 100644 index 0000000..36e8fcd --- /dev/null +++ b/src/main/java/org/metrics/model/ClassInfo.java @@ -0,0 +1,29 @@ +package org.metrics.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class ClassInfo { + public final String name; + public final String superName; + public final List interfaces; + public final boolean isInterface; + public final int fieldsCount; + + public final List methods = new ArrayList<>(); + + public int dit; + public int overriddenCount; + public int abcStoresTotal; + + public ClassInfo(String name, String superName, List interfaces, boolean isInterface, int fieldsCount) { + this.name = Objects.requireNonNull(name, "name"); + this.superName = superName; + this.interfaces = List.copyOf(interfaces); + this.isInterface = isInterface; + this.fieldsCount = fieldsCount; + } +} + + diff --git a/src/main/java/org/metrics/model/MethodInfo.java b/src/main/java/org/metrics/model/MethodInfo.java new file mode 100644 index 0000000..0d30325 --- /dev/null +++ b/src/main/java/org/metrics/model/MethodInfo.java @@ -0,0 +1,19 @@ +package org.metrics.model; + +import java.util.Objects; + +public final class MethodInfo { + public final String name; + public final String descriptor; + public final int access; + public int abcStores; + public boolean overrides; + + public MethodInfo(String name, String descriptor, int access) { + this.name = Objects.requireNonNull(name, "name"); + this.descriptor = Objects.requireNonNull(descriptor, "descriptor"); + this.access = access; + } +} + + diff --git a/src/main/java/org/metrics/model/Report.java b/src/main/java/org/metrics/model/Report.java new file mode 100644 index 0000000..743f7f4 --- /dev/null +++ b/src/main/java/org/metrics/model/Report.java @@ -0,0 +1,12 @@ +package org.metrics.model; + +import java.util.ArrayList; +import java.util.List; + +public final class Report { + public String project; + public Summary summary = new Summary(); + public List byClass = new ArrayList<>(); +} + + diff --git a/src/main/java/org/metrics/model/Summary.java b/src/main/java/org/metrics/model/Summary.java new file mode 100644 index 0000000..0db4626 --- /dev/null +++ b/src/main/java/org/metrics/model/Summary.java @@ -0,0 +1,31 @@ +package org.metrics.model; + +public final class Summary { + public int classesAnalyzed; + public int interfacesAnalyzed; + public InheritanceDepth inheritanceDepth = new InheritanceDepth(); + public AbcSummary abc = new AbcSummary(); + public OverridesSummary overrides = new OverridesSummary(); + public FieldsSummary fields = new FieldsSummary(); + + public static final class InheritanceDepth { + public int max; + public double avg; + } + + public static final class AbcSummary { + public int totalStores; + public double avgStoresPerClass; + public double avgStoresPerMethod; + } + + public static final class OverridesSummary { + public double avgOverriddenMethodsPerClass; + } + + public static final class FieldsSummary { + public double avgFieldsPerClass; + } +} + + diff --git a/src/main/java/org/metrics/output/ConsoleSummary.java b/src/main/java/org/metrics/output/ConsoleSummary.java new file mode 100644 index 0000000..63bc5a9 --- /dev/null +++ b/src/main/java/org/metrics/output/ConsoleSummary.java @@ -0,0 +1,25 @@ +package org.metrics.output; + +import org.metrics.model.Report; +import org.metrics.model.Summary; + +import java.text.DecimalFormat; + +public final class ConsoleSummary { + private static final DecimalFormat DF = new DecimalFormat("#.###"); + + public void print(Report report) { + Summary s = report.summary; + System.out.println("== Metrics Summary =="); + System.out.println("Project: " + report.project); + System.out.println("Analyzed: classes=" + s.classesAnalyzed + ", interfaces=" + s.interfacesAnalyzed); + System.out.println("DIT: max=" + s.inheritanceDepth.max + ", avg=" + DF.format(s.inheritanceDepth.avg)); + System.out.println("ABC stores: total=" + s.abc.totalStores + + ", avg/class=" + DF.format(s.abc.avgStoresPerClass) + + ", avg/method=" + DF.format(s.abc.avgStoresPerMethod)); + System.out.println("Overrides: avg/class=" + DF.format(s.overrides.avgOverriddenMethodsPerClass)); + System.out.println("Fields: avg/class=" + DF.format(s.fields.avgFieldsPerClass)); + } +} + + diff --git a/src/main/java/org/metrics/output/JsonWriter.java b/src/main/java/org/metrics/output/JsonWriter.java new file mode 100644 index 0000000..2ece7ac --- /dev/null +++ b/src/main/java/org/metrics/output/JsonWriter.java @@ -0,0 +1,28 @@ +package org.metrics.output; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.metrics.model.Report; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +public final class JsonWriter { + private final ObjectMapper mapper; + + public JsonWriter() { + this.mapper = new ObjectMapper(); + } + + public void write(Report report, Path output) throws IOException { + Report toWrite = report; + mapper.enable(SerializationFeature.INDENT_OUTPUT); + byte[] json = mapper.writeValueAsBytes(toWrite); + Files.createDirectories(output.toAbsolutePath().getParent()); + Files.write(output, json); + } +} + + diff --git a/src/main/java/org/metrics/visit/CollectClassVisitor.java b/src/main/java/org/metrics/visit/CollectClassVisitor.java new file mode 100644 index 0000000..4bc7ad4 --- /dev/null +++ b/src/main/java/org/metrics/visit/CollectClassVisitor.java @@ -0,0 +1,65 @@ +package org.metrics.visit; + +import org.metrics.model.ClassInfo; +import org.metrics.model.MethodInfo; +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.List; +import java.util.function.Consumer; + +public final class CollectClassVisitor extends ClassVisitor { + private final Consumer sink; + + private String name; + private String superName; + private final List interfaces = new ArrayList<>(); + private boolean isInterface; + private int fieldsCount; + private final List methods = new ArrayList<>(); + + public CollectClassVisitor(int api, Consumer sink) { + super(api); + this.sink = sink; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + this.name = name; + this.superName = superName; + this.isInterface = (access & Opcodes.ACC_INTERFACE) != 0; + if (interfaces != null) { + for (String itf : interfaces) { + this.interfaces.add(itf); + } + } + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + fieldsCount++; + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodInfo mi = new MethodInfo(name, descriptor, access); + methods.add(mi); + MethodVisitor delegate = super.visitMethod(access, name, descriptor, signature, exceptions); + return new CollectMethodVisitor(Opcodes.ASM9, delegate, mi); + } + + @Override + public void visitEnd() { + ClassInfo classInfo = new ClassInfo(name, superName, interfaces, isInterface, fieldsCount); + classInfo.methods.addAll(methods); + sink.accept(classInfo); + super.visitEnd(); + } +} + + diff --git a/src/main/java/org/metrics/visit/CollectMethodVisitor.java b/src/main/java/org/metrics/visit/CollectMethodVisitor.java new file mode 100644 index 0000000..dda4a5a --- /dev/null +++ b/src/main/java/org/metrics/visit/CollectMethodVisitor.java @@ -0,0 +1,32 @@ +package org.metrics.visit; + +import org.metrics.model.MethodInfo; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +public final class CollectMethodVisitor extends MethodVisitor { + private final MethodInfo methodInfo; + + public CollectMethodVisitor(int api, MethodVisitor delegate, MethodInfo methodInfo) { + super(api, delegate); + this.methodInfo = methodInfo; + } + + @Override + public void visitVarInsn(int opcode, int var) { + if (isStoreVarOpcode(opcode)) { + methodInfo.abcStores++; + } + super.visitVarInsn(opcode, var); + } + + private static boolean isStoreVarOpcode(int opcode) { + return opcode == Opcodes.ISTORE + || opcode == Opcodes.LSTORE + || opcode == Opcodes.FSTORE + || opcode == Opcodes.DSTORE + || opcode == Opcodes.ASTORE; + } +} + + diff --git a/src/test/java/org/metrics/CoreMetricsTest.java b/src/test/java/org/metrics/CoreMetricsTest.java new file mode 100644 index 0000000..f9770bc --- /dev/null +++ b/src/test/java/org/metrics/CoreMetricsTest.java @@ -0,0 +1,108 @@ +package org.metrics; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; +import org.metrics.analysis.HierarchyBuilder; +import org.metrics.analysis.MetricsAggregator; +import org.metrics.analysis.OverrideResolver; +import org.metrics.model.ClassInfo; +import org.metrics.model.MethodInfo; +import org.metrics.model.Report; +import org.metrics.model.Summary; +import org.metrics.visit.CollectMethodVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +public class CoreMetricsTest { + @Test + void testDitComputationSimpleChain() { + Map classes = new HashMap<>(); + classes.put("A", new ClassInfo("A", "java/lang/Object", List.of(), false, 0)); + classes.put("B", new ClassInfo("B", "A", List.of(), false, 0)); + classes.put("C", new ClassInfo("C", "B", List.of(), false, 0)); + + HierarchyBuilder hb = new HierarchyBuilder(); + hb.computeAllDit(classes, true); + + assertEquals(0, classes.get("A").dit); + assertEquals(1, classes.get("B").dit); + assertEquals(2, classes.get("C").dit); + } + + @Test + void testAbcCounting() { + MethodInfo mi = new MethodInfo("m", "()V", 0); + MethodVisitor stub = new MethodVisitor(Opcodes.ASM9) {}; + CollectMethodVisitor v = new CollectMethodVisitor(Opcodes.ASM9, stub, mi); + v.visitVarInsn(Opcodes.ISTORE, 1); + v.visitVarInsn(Opcodes.ASTORE, 2); + v.visitVarInsn(Opcodes.ISTORE, 0); + v.visitVarInsn(Opcodes.LSTORE, 3); + v.visitIincInsn(0, 1); + assertEquals(4, mi.abcStores); + } + + @Test + void testOverrideDetectionAcrossClassAndInterface() { + Map classes = new HashMap<>(); + ClassInfo iface = new ClassInfo("I", null, List.of(), true, 0); + MethodInfo iM = new MethodInfo("m", "()V", 0); + iface.methods.add(iM); + classes.put("I", iface); + + ClassInfo superC = new ClassInfo("S", "java/lang/Object", List.of("I"), false, 0); + MethodInfo sM = new MethodInfo("m", "()V", 0); + superC.methods.add(sM); + classes.put("S", superC); + + ClassInfo c = new ClassInfo("C", "S", List.of("I"), false, 0); + MethodInfo cM = new MethodInfo("m", "()V", 0); + c.methods.add(cM); + classes.put("C", c); + + HierarchyBuilder hb = new HierarchyBuilder(); + hb.computeAllDit(classes, true); + OverrideResolver or = new OverrideResolver(); + or.resolve(classes, true); + assertTrue(cM.overrides); + assertEquals(1, c.overriddenCount); + } + + @Test + void testAggregatorMetrics() { + Map classes = new HashMap<>(); + ClassInfo a = new ClassInfo("A", "java/lang/Object", List.of(), false, 2); + a.dit = 0; + MethodInfo am1 = new MethodInfo("m1", "()V", 0); + am1.abcStores = 2; + a.methods.add(am1); + classes.put("A", a); + + ClassInfo b = new ClassInfo("B", "A", List.of(), false, 4); + b.dit = 1; + MethodInfo bm1 = new MethodInfo("m1", "()V", 0); + bm1.abcStores = 1; + b.methods.add(bm1); + b.overriddenCount = 1; + classes.put("B", b); + + MetricsAggregator agg = new MetricsAggregator(); + Report r = agg.aggregate("test.jar", classes); + Summary s = r.summary; + assertEquals(2, s.classesAnalyzed); + assertEquals(1, s.inheritanceDepth.max); + assertEquals(0.5, s.inheritanceDepth.avg); + assertEquals(3, s.abc.totalStores); + assertEquals(1.5, s.abc.avgStoresPerClass); + assertEquals(1.5, s.abc.avgStoresPerMethod); + assertEquals(0.5, s.overrides.avgOverriddenMethodsPerClass); + assertEquals(3.0, s.fields.avgFieldsPerClass); + } +} + + diff --git a/src/test/java/org/metrics/JsonSnapshotTest.java b/src/test/java/org/metrics/JsonSnapshotTest.java new file mode 100644 index 0000000..12c187f --- /dev/null +++ b/src/test/java/org/metrics/JsonSnapshotTest.java @@ -0,0 +1,54 @@ +package org.metrics; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; +import org.metrics.analysis.MetricsAggregator; +import org.metrics.model.ClassInfo; +import org.metrics.model.MethodInfo; +import org.metrics.model.Report; +import org.metrics.output.JsonWriter; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class JsonSnapshotTest { + @Test + void simpleSnapshot() throws Exception { + Map classes = new LinkedHashMap<>(); + ClassInfo a = new ClassInfo("A", "java/lang/Object", List.of(), false, 2); + a.dit = 0; + MethodInfo am1 = new MethodInfo("m1", "()V", 0); + am1.abcStores = 2; + a.methods.add(am1); + classes.put("A", a); + + ClassInfo b = new ClassInfo("B", "A", List.of(), false, 4); + b.dit = 1; + MethodInfo bm1 = new MethodInfo("m1", "()V", 0); + bm1.abcStores = 1; + b.methods.add(bm1); + b.overriddenCount = 1; + classes.put("B", b); + + MetricsAggregator agg = new MetricsAggregator(); + Report r = agg.aggregate("test.jar", classes); + + Path tmp = Files.createTempFile("snapshot", ".json"); + new JsonWriter().write(r, tmp); + + ObjectMapper mapper = new ObjectMapper(); + var actual = mapper.readTree(Files.readString(tmp)); + try (InputStream in = getClass().getResourceAsStream("/snapshots/simple.json")) { + var expected = mapper.readTree(in); + assertEquals(expected, actual); + } + } +} + + diff --git a/src/test/resources/snapshots/simple.json b/src/test/resources/snapshots/simple.json new file mode 100644 index 0000000..d1f47cb --- /dev/null +++ b/src/test/resources/snapshots/simple.json @@ -0,0 +1,49 @@ +{ + "project" : "test.jar", + "summary" : { + "classesAnalyzed" : 2, + "interfacesAnalyzed" : 0, + "inheritanceDepth" : { + "max" : 1, + "avg" : 0.5 + }, + "abc" : { + "totalStores" : 3, + "avgStoresPerClass" : 1.5, + "avgStoresPerMethod" : 1.5 + }, + "overrides" : { + "avgOverriddenMethodsPerClass" : 0.5 + }, + "fields" : { + "avgFieldsPerClass" : 3.0 + } + }, + "byClass" : [ { + "name" : "A", + "isInterface" : false, + "superName" : "java/lang/Object", + "interfaces" : [ ], + "dit" : 0, + "fieldsCount" : 2, + "methods" : { + "declared" : 1, + "overridden" : 0, + "abcStores" : 2 + } + }, { + "name" : "B", + "isInterface" : false, + "superName" : "A", + "interfaces" : [ ], + "dit" : 1, + "fieldsCount" : 4, + "methods" : { + "declared" : 1, + "overridden" : 1, + "abcStores" : 1 + } + } ] +} + + From 44ea790d02e1f1fe1ddf053c23526a7749db796b Mon Sep 17 00:00:00 2001 From: Vlad Denisov Date: Tue, 16 Dec 2025 21:47:10 +0300 Subject: [PATCH 3/3] feat: add BC metric --- .../metrics/analysis/HierarchyBuilder.java | 3 +- .../metrics/analysis/MetricsAggregator.java | 18 ++++++++++ src/main/java/org/metrics/cli/Main.java | 28 --------------- src/main/java/org/metrics/model/ByClass.java | 2 ++ .../java/org/metrics/model/ClassInfo.java | 2 ++ .../java/org/metrics/model/MethodInfo.java | 2 ++ src/main/java/org/metrics/model/Summary.java | 6 ++++ .../org/metrics/output/ConsoleSummary.java | 12 +++++-- .../metrics/visit/CollectClassVisitor.java | 5 ++- .../metrics/visit/CollectMethodVisitor.java | 36 +++++++++++++++++-- .../java/org/metrics/CoreMetricsTest.java | 12 +++++++ src/test/resources/snapshots/simple.json | 16 +++++++-- 12 files changed, 101 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/metrics/analysis/HierarchyBuilder.java b/src/main/java/org/metrics/analysis/HierarchyBuilder.java index 8992153..0f35a88 100644 --- a/src/main/java/org/metrics/analysis/HierarchyBuilder.java +++ b/src/main/java/org/metrics/analysis/HierarchyBuilder.java @@ -73,7 +73,7 @@ public List ancestorsOf(String className, Map classes result.add(parent); current = parent; } - // интерфейсы по цепочке (bfs) + // интерфейсы по цепочке Set visited = new HashSet<>(); Deque queue = new ArrayDeque<>(); ClassInfo root = classesByName.get(className); @@ -101,6 +101,7 @@ public List ancestorsOf(String className, Map classes } current = parent; } + // интерфейсы по цепочке (bfs) while (!queue.isEmpty()) { String itf = queue.removeFirst(); result.add(itf); diff --git a/src/main/java/org/metrics/analysis/MetricsAggregator.java b/src/main/java/org/metrics/analysis/MetricsAggregator.java index 91eff55..128633f 100644 --- a/src/main/java/org/metrics/analysis/MetricsAggregator.java +++ b/src/main/java/org/metrics/analysis/MetricsAggregator.java @@ -19,6 +19,8 @@ public Report aggregate(String projectName, Map classesByName int maxDit = 0; long ditSum = 0L; long totalAbcStores = 0L; + long totalAbcBranches = 0L; + long totalAbcConditions = 0L; long totalMethods = 0L; long totalOverridden = 0L; long totalFields = 0L; @@ -34,13 +36,21 @@ public Report aggregate(String projectName, Map classesByName totalFields += ci.fieldsCount; } int classMethodStores = 0; + int classMethodBranches = 0; + int classMethodConditions = 0; int declared = ci.methods.size(); int overridden = ci.overriddenCount; for (MethodInfo mi : ci.methods) { classMethodStores += mi.abcStores; + classMethodBranches += mi.abcBranches; + classMethodConditions += mi.abcConditions; } ci.abcStoresTotal = classMethodStores; + ci.abcBranchesTotal = classMethodBranches; + ci.abcConditionsTotal = classMethodConditions; totalAbcStores += classMethodStores; + totalAbcBranches += classMethodBranches; + totalAbcConditions += classMethodConditions; totalMethods += declared; totalOverridden += overridden; @@ -54,6 +64,8 @@ public Report aggregate(String projectName, Map classesByName bc.methods.declared = declared; bc.methods.overridden = overridden; bc.methods.abcStores = classMethodStores; + bc.methods.abcBranches = classMethodBranches; + bc.methods.abcConditions = classMethodConditions; byClassList.add(bc); } @@ -62,8 +74,14 @@ public Report aggregate(String projectName, Map classesByName summary.inheritanceDepth.max = maxDit; summary.inheritanceDepth.avg = classesCount == 0 ? 0.0 : (double) ditSum / classesCount; summary.abc.totalStores = (int) totalAbcStores; + summary.abc.totalBranches = (int) totalAbcBranches; + summary.abc.totalConditions = (int) totalAbcConditions; summary.abc.avgStoresPerClass = classesCount == 0 ? 0.0 : (double) totalAbcStores / classesCount; summary.abc.avgStoresPerMethod = totalMethods == 0 ? 0.0 : (double) totalAbcStores / totalMethods; + summary.abc.avgBranchesPerClass = classesCount == 0 ? 0.0 : (double) totalAbcBranches / classesCount; + summary.abc.avgBranchesPerMethod = totalMethods == 0 ? 0.0 : (double) totalAbcBranches / totalMethods; + summary.abc.avgConditionsPerClass = classesCount == 0 ? 0.0 : (double) totalAbcConditions / classesCount; + summary.abc.avgConditionsPerMethod = totalMethods == 0 ? 0.0 : (double) totalAbcConditions / totalMethods; summary.overrides.avgOverriddenMethodsPerClass = classesCount == 0 ? 0.0 : (double) totalOverridden / classesCount; summary.fields.avgFieldsPerClass = classesCount == 0 ? 0.0 : (double) totalFields / classesCount; report.byClass = byClassList; diff --git a/src/main/java/org/metrics/cli/Main.java b/src/main/java/org/metrics/cli/Main.java index 16c03ec..e32850c 100644 --- a/src/main/java/org/metrics/cli/Main.java +++ b/src/main/java/org/metrics/cli/Main.java @@ -39,9 +39,6 @@ public final class Main implements Callable { @Option(names = {"-o", "--json"}, description = "Path to write JSON report") private Path jsonOutput; - @Option(names = {"-cp", "--classpath"}, description = "Additional classpath (e.g. \"lib/*:rt.jar\")") - private String classpath; - @Option(names = {"--fail-on-missing-super"}, description = "Fail if a superclass/interface cannot be resolved") private boolean failOnMissingSuper; @@ -61,31 +58,6 @@ public Integer call() { } }); - // загружаем внешних предков из classpath - if (classpath != null && !classpath.isBlank()) { - try (ClasspathLoader loader = new ClasspathLoader(classpath)) { - Deque queue = new ArrayDeque<>(); - Set seen = new HashSet<>(classesByName.keySet()); - for (ClassInfo ci : classesByName.values()) { - if (ci.superName != null) queue.add(ci.superName); - for (String itf : ci.interfaces) queue.add(itf); - } - while (!queue.isEmpty()) { - String name = queue.removeFirst(); - if (name == null || "java/lang/Object".equals(name)) continue; - if (!seen.add(name)) continue; - ClassInfo ext = loader.load(name); - if (ext != null) { - classesByName.put(ext.name, ext); - if (ext.superName != null) queue.add(ext.superName); - for (String itf : ext.interfaces) queue.add(itf); - } else if (failOnMissingSuper) { - throw new IllegalStateException("Missing class on classpath: " + name); - } - } - } - } - // считаем dit и переопределения HierarchyBuilder hierarchy = new HierarchyBuilder(); hierarchy.computeAllDit(classesByName, failOnMissingSuper); diff --git a/src/main/java/org/metrics/model/ByClass.java b/src/main/java/org/metrics/model/ByClass.java index c109644..8d3b398 100644 --- a/src/main/java/org/metrics/model/ByClass.java +++ b/src/main/java/org/metrics/model/ByClass.java @@ -16,6 +16,8 @@ public static final class MethodsSummary { public int declared; public int overridden; public int abcStores; + public int abcBranches; + public int abcConditions; } } diff --git a/src/main/java/org/metrics/model/ClassInfo.java b/src/main/java/org/metrics/model/ClassInfo.java index 36e8fcd..93b4b2f 100644 --- a/src/main/java/org/metrics/model/ClassInfo.java +++ b/src/main/java/org/metrics/model/ClassInfo.java @@ -16,6 +16,8 @@ public final class ClassInfo { public int dit; public int overriddenCount; public int abcStoresTotal; + public int abcBranchesTotal; + public int abcConditionsTotal; public ClassInfo(String name, String superName, List interfaces, boolean isInterface, int fieldsCount) { this.name = Objects.requireNonNull(name, "name"); diff --git a/src/main/java/org/metrics/model/MethodInfo.java b/src/main/java/org/metrics/model/MethodInfo.java index 0d30325..55a582c 100644 --- a/src/main/java/org/metrics/model/MethodInfo.java +++ b/src/main/java/org/metrics/model/MethodInfo.java @@ -7,6 +7,8 @@ public final class MethodInfo { public final String descriptor; public final int access; public int abcStores; + public int abcBranches; + public int abcConditions; public boolean overrides; public MethodInfo(String name, String descriptor, int access) { diff --git a/src/main/java/org/metrics/model/Summary.java b/src/main/java/org/metrics/model/Summary.java index 0db4626..f4aac84 100644 --- a/src/main/java/org/metrics/model/Summary.java +++ b/src/main/java/org/metrics/model/Summary.java @@ -15,8 +15,14 @@ public static final class InheritanceDepth { public static final class AbcSummary { public int totalStores; + public int totalBranches; + public int totalConditions; public double avgStoresPerClass; public double avgStoresPerMethod; + public double avgBranchesPerClass; + public double avgBranchesPerMethod; + public double avgConditionsPerClass; + public double avgConditionsPerMethod; } public static final class OverridesSummary { diff --git a/src/main/java/org/metrics/output/ConsoleSummary.java b/src/main/java/org/metrics/output/ConsoleSummary.java index 63bc5a9..ee35f9b 100644 --- a/src/main/java/org/metrics/output/ConsoleSummary.java +++ b/src/main/java/org/metrics/output/ConsoleSummary.java @@ -14,9 +14,15 @@ public void print(Report report) { System.out.println("Project: " + report.project); System.out.println("Analyzed: classes=" + s.classesAnalyzed + ", interfaces=" + s.interfacesAnalyzed); System.out.println("DIT: max=" + s.inheritanceDepth.max + ", avg=" + DF.format(s.inheritanceDepth.avg)); - System.out.println("ABC stores: total=" + s.abc.totalStores - + ", avg/class=" + DF.format(s.abc.avgStoresPerClass) - + ", avg/method=" + DF.format(s.abc.avgStoresPerMethod)); + System.out.println("ABC: A(stores)=" + s.abc.totalStores + + ", B(branches)=" + s.abc.totalBranches + + ", C(conditions)=" + s.abc.totalConditions); + System.out.println("ABC avg/class: A=" + DF.format(s.abc.avgStoresPerClass) + + ", B=" + DF.format(s.abc.avgBranchesPerClass) + + ", C=" + DF.format(s.abc.avgConditionsPerClass)); + System.out.println("ABC avg/method: A=" + DF.format(s.abc.avgStoresPerMethod) + + ", B=" + DF.format(s.abc.avgBranchesPerMethod) + + ", C=" + DF.format(s.abc.avgConditionsPerMethod)); System.out.println("Overrides: avg/class=" + DF.format(s.overrides.avgOverriddenMethodsPerClass)); System.out.println("Fields: avg/class=" + DF.format(s.fields.avgFieldsPerClass)); } diff --git a/src/main/java/org/metrics/visit/CollectClassVisitor.java b/src/main/java/org/metrics/visit/CollectClassVisitor.java index 4bc7ad4..3233898 100644 --- a/src/main/java/org/metrics/visit/CollectClassVisitor.java +++ b/src/main/java/org/metrics/visit/CollectClassVisitor.java @@ -8,6 +8,7 @@ import org.objectweb.asm.Opcodes; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Consumer; @@ -32,9 +33,7 @@ public void visit(int version, int access, String name, String signature, String this.superName = superName; this.isInterface = (access & Opcodes.ACC_INTERFACE) != 0; if (interfaces != null) { - for (String itf : interfaces) { - this.interfaces.add(itf); - } + this.interfaces.addAll(Arrays.asList(interfaces)); } super.visit(version, access, name, signature, superName, interfaces); } diff --git a/src/main/java/org/metrics/visit/CollectMethodVisitor.java b/src/main/java/org/metrics/visit/CollectMethodVisitor.java index dda4a5a..2c3e03b 100644 --- a/src/main/java/org/metrics/visit/CollectMethodVisitor.java +++ b/src/main/java/org/metrics/visit/CollectMethodVisitor.java @@ -1,6 +1,7 @@ package org.metrics.visit; import org.metrics.model.MethodInfo; +import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; @@ -13,13 +14,13 @@ public CollectMethodVisitor(int api, MethodVisitor delegate, MethodInfo methodIn } @Override - public void visitVarInsn(int opcode, int var) { + public void visitVarInsn(int opcode, int varIndex) { if (isStoreVarOpcode(opcode)) { methodInfo.abcStores++; } - super.visitVarInsn(opcode, var); + super.visitVarInsn(opcode, varIndex); } - + private static boolean isStoreVarOpcode(int opcode) { return opcode == Opcodes.ISTORE || opcode == Opcodes.LSTORE @@ -27,6 +28,35 @@ private static boolean isStoreVarOpcode(int opcode) { || opcode == Opcodes.DSTORE || opcode == Opcodes.ASTORE; } + + @Override + public void visitJumpInsn(int opcode, Label label) { + if (opcode == Opcodes.GOTO) { + methodInfo.abcBranches++; + } else if (isConditionalJumpOpcode(opcode)) { + methodInfo.abcConditions++; + } + super.visitJumpInsn(opcode, label); + } + + private static boolean isConditionalJumpOpcode(int opcode) { + return 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; + } } diff --git a/src/test/java/org/metrics/CoreMetricsTest.java b/src/test/java/org/metrics/CoreMetricsTest.java index f9770bc..46ec66c 100644 --- a/src/test/java/org/metrics/CoreMetricsTest.java +++ b/src/test/java/org/metrics/CoreMetricsTest.java @@ -44,7 +44,13 @@ void testAbcCounting() { v.visitVarInsn(Opcodes.ISTORE, 0); v.visitVarInsn(Opcodes.LSTORE, 3); v.visitIincInsn(0, 1); + var label = new org.objectweb.asm.Label(); + v.visitJumpInsn(Opcodes.GOTO, label); + v.visitJumpInsn(Opcodes.IFEQ, label); + v.visitJumpInsn(Opcodes.IFNULL, label); assertEquals(4, mi.abcStores); + assertEquals(1, mi.abcBranches); + assertEquals(2, mi.abcConditions); } @Test @@ -98,8 +104,14 @@ void testAggregatorMetrics() { assertEquals(1, s.inheritanceDepth.max); assertEquals(0.5, s.inheritanceDepth.avg); assertEquals(3, s.abc.totalStores); + assertEquals(0, s.abc.totalBranches); + assertEquals(0, s.abc.totalConditions); assertEquals(1.5, s.abc.avgStoresPerClass); assertEquals(1.5, s.abc.avgStoresPerMethod); + assertEquals(0.0, s.abc.avgBranchesPerClass); + assertEquals(0.0, s.abc.avgBranchesPerMethod); + assertEquals(0.0, s.abc.avgConditionsPerClass); + assertEquals(0.0, s.abc.avgConditionsPerMethod); assertEquals(0.5, s.overrides.avgOverriddenMethodsPerClass); assertEquals(3.0, s.fields.avgFieldsPerClass); } diff --git a/src/test/resources/snapshots/simple.json b/src/test/resources/snapshots/simple.json index d1f47cb..0b4b08c 100644 --- a/src/test/resources/snapshots/simple.json +++ b/src/test/resources/snapshots/simple.json @@ -9,8 +9,14 @@ }, "abc" : { "totalStores" : 3, + "totalBranches" : 0, + "totalConditions" : 0, "avgStoresPerClass" : 1.5, - "avgStoresPerMethod" : 1.5 + "avgStoresPerMethod" : 1.5, + "avgBranchesPerClass" : 0.0, + "avgBranchesPerMethod" : 0.0, + "avgConditionsPerClass" : 0.0, + "avgConditionsPerMethod" : 0.0 }, "overrides" : { "avgOverriddenMethodsPerClass" : 0.5 @@ -29,7 +35,9 @@ "methods" : { "declared" : 1, "overridden" : 0, - "abcStores" : 2 + "abcStores" : 2, + "abcBranches" : 0, + "abcConditions" : 0 } }, { "name" : "B", @@ -41,7 +49,9 @@ "methods" : { "declared" : 1, "overridden" : 1, - "abcStores" : 1 + "abcStores" : 1, + "abcBranches" : 0, + "abcConditions" : 0 } } ] }