Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 файла
Expand Down
7 changes: 7 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ dependencies {
implementation("org.ow2.asm:asm-analysis:9.5")
implementation("org.ow2.asm:asm-util:9.5")

implementation("com.fasterxml.jackson.core:jackson-databind:2.16.0")
implementation("org.slf4j:slf4j-api:2.0.17")
implementation("ch.qos.logback:logback-classic:1.5.19")

compileOnly("org.projectlombok:lombok:1.18.30")
annotationProcessor("org.projectlombok:lombok:1.18.30")

testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
75 changes: 75 additions & 0 deletions src/main/java/org/example/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.example;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

import org.example.analyzer.JarAnalyzer;
import org.example.analyzer.MetricsCalculator;
import org.example.model.ClassInfo;
import org.example.model.MetricsResult;
import org.example.output.ConsoleMetricsPrinter;
import org.example.output.JsonMetricsPrinter;
import org.example.output.MetricsPrinter;

public class Main {
public static void main(String[] args) {
String inputPath = null;
String outputPath = null;

for (int i = 0; i < args.length; i++) {
switch (args[i]) {
case "--input" -> {
if (i + 1 < args.length) {
inputPath = args[++i];
}
}
case "--output" -> {
if (i + 1 < args.length) {
outputPath = args[++i];
}
}
default -> throw new IllegalArgumentException("Unknown option: " + args[i]);
}
}

Path jarPath = getValidJarPath(inputPath);

try {
analyzeJarInternal(jarPath, outputPath);
} catch (IOException e) {
throw new RuntimeException("Error analyzing jar " + jarPath + ": " + e.getMessage(), e);
}
}

private static Path getValidJarPath(String inputPath) {
if (inputPath == null) {
throw new IllegalArgumentException("JAR file is not provided");
}

Path jarPath = Path.of(inputPath);
if (!Files.exists(jarPath)) {
throw new IllegalArgumentException("JAR file does not exist: " + jarPath);
}

return jarPath;
}

private static void analyzeJarInternal(Path jarPath, String outputPath) throws IOException {
JarAnalyzer jarAnalyzer = new JarAnalyzer();
MetricsCalculator calculator = new MetricsCalculator();

Map<String, ClassInfo> classes = jarAnalyzer.getJarClassInfo(jarPath);
MetricsResult result = calculator.calculate(classes, jarPath.getFileName().toString());

MetricsPrinter consoleMetricsPrinter = new ConsoleMetricsPrinter();
consoleMetricsPrinter.print(result);

if (outputPath != null) {
MetricsPrinter jsonMetricsPrinter = new JsonMetricsPrinter(Path.of(outputPath));
jsonMetricsPrinter.print(result);
}
}
}

60 changes: 60 additions & 0 deletions src/main/java/org/example/analyzer/JarAnalyzer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.example.analyzer;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.example.model.ClassInfo;
import org.example.visitor.ClassInfoCollector;
import org.objectweb.asm.ClassReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JarAnalyzer {
private static final Logger LOGGER = LoggerFactory.getLogger(JarAnalyzer.class);

private static final String CLASS_EXTENSION = ".class";
private static final Set<String> META_CLASS_NAMES = Set.of("module-info.class", "package-info.class");

public Map<String, ClassInfo> getJarClassInfo(Path jarPath) throws IOException {
Map<String, ClassInfo> classes = new HashMap<>();

try (JarFile jarFile = new JarFile(jarPath.toFile())) {
var entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String entryName = entry.getName();

if (!entryName.endsWith(CLASS_EXTENSION)) {
continue;
}

if (META_CLASS_NAMES.stream().anyMatch(entryName::endsWith)) {
continue;
}

try (InputStream inputStream = jarFile.getInputStream(entry)) {
ClassInfo classInfo = analyzeClass(inputStream);
classes.put(classInfo.name(), classInfo);
} catch (Exception e) {
LOGGER.error("Error while analyzing class {}: {}", entryName, e.getMessage(), e);
}
}
}

return classes;
}

private ClassInfo analyzeClass(InputStream classInputStream) throws IOException {
ClassReader classReader = new ClassReader(classInputStream);
ClassInfoCollector collector = new ClassInfoCollector();
classReader.accept(collector, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
return collector.getResult();
}
}

207 changes: 207 additions & 0 deletions src/main/java/org/example/analyzer/MetricsCalculator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package org.example.analyzer;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.example.model.ClassInfo;
import org.example.model.MethodInfo;
import org.example.model.MetricsResult;

public class MetricsCalculator {
private static final String OBJECT_CLASS_NAME = "java/lang/Object";

public MetricsResult calculate(Map<String, ClassInfo> classes, String jarFileName) {
var regularClasses = classes.values().stream()
.filter(c -> !c.isInterface())
.toList();

if (regularClasses.isEmpty()) {
return MetricsResult.builder()
.jarFileName(jarFileName)
.build();
}

Map<String, Integer> depthCache = new HashMap<>();
int maxDepth = 0;
int totalDepth = 0;

for (ClassInfo classInfo : regularClasses) {
int depth = calculateDepth(classInfo.name(), classes, depthCache);
maxDepth = Math.max(maxDepth, depth);
totalDepth += depth;
}
double avgDepth = (double) totalDepth / regularClasses.size();

long totalAssignments = 0;
long totalBranches = 0;
long totalConditions = 0;
int totalMethods = 0;
int totalFields = 0;

for (ClassInfo classInfo : classes.values()) {
for (MethodInfo method : classInfo.methods()) {
totalAssignments += method.assignments();
totalBranches += method.branches();
totalConditions += method.conditions();
totalMethods++;
}
totalFields += classInfo.fieldCount();
}

double abcMetric = Math.sqrt(
(double) totalAssignments * totalAssignments +
(double) totalBranches * totalBranches +
(double) totalConditions * totalConditions
);

double avgFields = (double) totalFields / classes.size();

Map<String, Set<String>> parentMethodsCache = new HashMap<>();
int totalOverridden = 0;
for (ClassInfo classInfo : regularClasses) {
totalOverridden += countOverriddenMethods(classInfo, classes, parentMethodsCache);
}
double avgOverridden = (double) totalOverridden / regularClasses.size();

return MetricsResult.builder()
.jarFileName(jarFileName)
.totalClasses(classes.size())
.totalMethods(totalMethods)
.totalFields(totalFields)
.maxInheritanceDepth(maxDepth)
.avgInheritanceDepth(avgDepth)
.totalAssignments(totalAssignments)
.totalBranches(totalBranches)
.totalConditions(totalConditions)
.abcMetric(abcMetric)
.avgOverriddenMethods(avgOverridden)
.avgFieldsPerClass(avgFields)
.build();
}

private int calculateDepth(
String className,
Map<String, ClassInfo> classes,
Map<String, Integer> cache
) {
if (className == null || OBJECT_CLASS_NAME.equals(className)) {
return 0;
}

if (cache.containsKey(className)) {
return cache.get(className);
}

ClassInfo classInfo = classes.get(className);

int depth;
if (classInfo != null) {
depth = 1 + calculateDepth(classInfo.superName(), classes, cache);
} else {
depth = 1;
}

cache.put(className, depth);
return depth;
}

private int countOverriddenMethods(
ClassInfo classInfo,
Map<String, ClassInfo> classes,
Map<String, Set<String>> cache
) {
Set<String> parentMethods = new HashSet<>(
collectSuperClassMethods(classInfo.superName(), classes, cache)
);

for (String iface : classInfo.interfaces()) {
parentMethods.addAll(collectInterfaceMethods(iface, classes, cache));
}

int overridden = 0;
for (MethodInfo method : classInfo.methods()) {
if (!method.canBeOverride()) {
continue;
}

String signature = method.name() + method.descriptor();
if (parentMethods.contains(signature)) {
overridden++;
}
}

return overridden;
}

private Set<String> collectSuperClassMethods(
String superName,
Map<String, ClassInfo> classes,
Map<String, Set<String>> cache
) {
if (superName == null || OBJECT_CLASS_NAME.equals(superName)) {
return Collections.emptySet();
}

if (cache.containsKey(superName)) {
return cache.get(superName);
}

ClassInfo parent = classes.get(superName);
if (parent == null) {
return Collections.emptySet();
}

Set<String> methods = new HashSet<>(collectSuperClassMethods(parent.superName(), classes, cache));

for (String iface : parent.interfaces()) {
methods.addAll(collectInterfaceMethods(iface, classes, cache));
}

for (MethodInfo method : parent.methods()) {
if (method.isOverrideable()) {
methods.add(method.name() + method.descriptor());
}
}

cache.put(superName, methods);
return methods;
}

private Set<String> collectInterfaceMethods(
String ifaceName,
Map<String, ClassInfo> classes,
Map<String, Set<String>> cache
) {
if (ifaceName == null) {
return Collections.emptySet();
}

if (cache.containsKey(ifaceName)) {
return cache.get(ifaceName);
}

ClassInfo iface = classes.get(ifaceName);
if (iface == null) {
return Collections.emptySet();
}

Set<String> methods = new HashSet<>();

for (String parentIface : iface.interfaces()) {
methods.addAll(collectInterfaceMethods(parentIface, classes, cache));
}

for (MethodInfo method : iface.methods()) {
if (!method.isConstructor() && !method.isStaticInitializer()) {
methods.add(method.name() + method.descriptor());
}
}

cache.put(ifaceName, methods);
return methods;
}
}

19 changes: 19 additions & 0 deletions src/main/java/org/example/model/ClassInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.example.model;

import java.util.List;

import static org.objectweb.asm.Opcodes.ACC_INTERFACE;

public record ClassInfo(
String name,
String superName,
List<String> interfaces,
int accessFlags,
int fieldCount,
List<MethodInfo> methods
) {
public boolean isInterface() {
return (accessFlags & ACC_INTERFACE) != 0;
}
}

Loading