Code Metrics Calculator in Java

Introduction

A comprehensive Code Metrics Calculator that analyzes Java source code to provide various software metrics including complexity, maintainability, and quality indicators.

Features

  • Line Metrics: Count lines of code, comments, blanks
  • Complexity Metrics: Cyclomatic complexity, method count
  • Object-Oriented Metrics: Inheritance depth, coupling, cohesion
  • Maintainability Index: Calculate code maintainability
  • Halstead Metrics: Program volume, difficulty, effort
  • Code Smell Detection: Identify potential issues

Core Implementation

Main Metrics Calculator Class

package com.codemetrics;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.regex.*;
import java.util.stream.*;
public class CodeMetricsCalculator {
private final List<File> javaFiles;
private final MetricsResult totalMetrics;
private final List<FileMetrics> fileMetrics;
public CodeMetricsCalculator(String directoryPath) throws IOException {
this.javaFiles = findJavaFiles(directoryPath);
this.totalMetrics = new MetricsResult();
this.fileMetrics = new ArrayList<>();
}
public CodeMetricsCalculator(List<File> javaFiles) {
this.javaFiles = javaFiles;
this.totalMetrics = new MetricsResult();
this.fileMetrics = new ArrayList<>();
}
private List<File> findJavaFiles(String directoryPath) throws IOException {
Path startDir = Paths.get(directoryPath);
if (!Files.exists(startDir) || !Files.isDirectory(startDir)) {
throw new IllegalArgumentException("Directory does not exist: " + directoryPath);
}
try (Stream<Path> paths = Files.walk(startDir)) {
return paths
.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".java"))
.map(Path::toFile)
.collect(Collectors.toList());
}
}
public AnalysisResult analyze() {
System.out.println("Analyzing " + javaFiles.size() + " Java files...");
for (File javaFile : javaFiles) {
try {
FileMetrics metrics = analyzeFile(javaFile);
fileMetrics.add(metrics);
totalMetrics.aggregate(metrics.getMetrics());
} catch (IOException e) {
System.err.println("Error analyzing file: " + javaFile.getPath() + " - " + e.getMessage());
}
}
return new AnalysisResult(fileMetrics, totalMetrics);
}
private FileMetrics analyzeFile(File javaFile) throws IOException {
String content = new String(Files.readAllBytes(javaFile.toPath()));
FileMetrics metrics = new FileMetrics(javaFile.getName(), javaFile.getPath());
// Basic line metrics
calculateLineMetrics(content, metrics);
// Complexity metrics
calculateComplexityMetrics(content, metrics);
// OO metrics
calculateOOMetrics(content, metrics);
// Halstead metrics
calculateHalsteadMetrics(content, metrics);
// Maintainability index
calculateMaintainabilityIndex(metrics);
// Code smells
detectCodeSmells(content, metrics);
return metrics;
}
private void calculateLineMetrics(String content, FileMetrics metrics) {
String[] lines = content.split("\r\n|\r|\n");
int totalLines = lines.length;
int blankLines = 0;
int commentLines = 0;
int codeLines = 0;
boolean inBlockComment = false;
for (String line : lines) {
String trimmedLine = line.trim();
if (trimmedLine.isEmpty()) {
blankLines++;
continue;
}
if (inBlockComment) {
commentLines++;
if (trimmedLine.contains("*/")) {
inBlockComment = false;
}
continue;
}
if (trimmedLine.startsWith("/*")) {
commentLines++;
inBlockComment = !trimmedLine.contains("*/");
continue;
}
if (trimmedLine.startsWith("//")) {
commentLines++;
continue;
}
// Check for lines with code and comments
if (trimmedLine.contains("//")) {
commentLines++;
codeLines++; // Also counts as code line
continue;
}
codeLines++;
}
metrics.setLineMetrics(totalLines, blankLines, commentLines, codeLines);
}
private void calculateComplexityMetrics(String content, FileMetrics metrics) {
// Count methods
int methodCount = countMethods(content);
metrics.setMethodCount(methodCount);
// Calculate cyclomatic complexity
int complexity = calculateCyclomaticComplexity(content);
metrics.setCyclomaticComplexity(complexity);
// Average complexity per method
double avgComplexity = methodCount > 0 ? (double) complexity / methodCount : 0;
metrics.setAverageComplexity(avgComplexity);
}
private int countMethods(String content) {
// Pattern to match method declarations
Pattern methodPattern = Pattern.compile(
"(public|protected|private|static|\\s) +[\\w\\<\\>\\[\\]]+\\s+(\\w+) *\\([^)]*\\) *\\{? *\\n?",
Pattern.MULTILINE
);
Matcher matcher = methodPattern.matcher(content);
int count = 0;
while (matcher.find()) {
// Exclude constructors and simple getters/setters for more accurate count
String methodName = matcher.group(2);
if (!methodName.matches("^[A-Z].*") && // Not constructor
!methodName.matches("^(get|set|is).*")) { // Not simple accessor
count++;
}
}
return Math.max(count, 1); // At least 1 for the class itself
}
private int calculateCyclomaticComplexity(String content) {
int complexity = 1; // Base complexity
// Patterns for decision points
String[] decisionPatterns = {
"\\bif\\s*\\(", "\\belse\\s*\\{", "\\bfor\\s*\\(", "\\bwhile\\s*\\(", 
"\\bcase\\b", "\\bdefault\\b", "\\bcatch\\s*\\(", "\\b&&\\b", "\\b\\|\\|\\b",
"\\?\\s*[^:]*\\s*:"
};
for (String pattern : decisionPatterns) {
Pattern p = Pattern.compile(pattern);
Matcher matcher = p.matcher(content);
while (matcher.find()) {
complexity++;
}
}
return complexity;
}
private void calculateOOMetrics(String content, FileMetrics metrics) {
// Count classes
int classCount = countPattern(content, "\\bclass\\s+\\w+");
metrics.setClassCount(classCount);
// Count interfaces
int interfaceCount = countPattern(content, "\\binterface\\s+\\w+");
metrics.setInterfaceCount(interfaceCount);
// Estimate inheritance depth (simplified)
int inheritanceDepth = estimateInheritanceDepth(content);
metrics.setInheritanceDepth(inheritanceDepth);
// Count dependencies (import statements)
int dependencyCount = countPattern(content, "^import\\s+.*", Pattern.MULTILINE);
metrics.setDependencyCount(dependencyCount);
}
private int countPattern(String content, String pattern) {
return countPattern(content, pattern, 0);
}
private int countPattern(String content, String pattern, int flags) {
Pattern p = Pattern.compile(pattern, flags);
Matcher matcher = p.matcher(content);
int count = 0;
while (matcher.find()) {
count++;
}
return count;
}
private int estimateInheritanceDepth(String content) {
// Simplified estimation - count "extends" and "implements"
int extendsCount = countPattern(content, "\\bextends\\b");
int implementsCount = countPattern(content, "\\bimplements\\b");
return extendsCount + implementsCount;
}
private void calculateHalsteadMetrics(String content, FileMetrics metrics) {
HalsteadMetrics halstead = new HalsteadMetrics();
// Extract operators and operands (simplified)
Set<String> operators = extractOperators(content);
Set<String> operands = extractOperands(content);
halstead.setUniqueOperators(operators.size());
halstead.setUniqueOperands(operands.size());
// Count total occurrences (simplified)
int totalOperators = countTotalOccurrences(content, operators);
int totalOperands = countTotalOccurrences(content, operands);
halstead.setTotalOperators(totalOperators);
halstead.setTotalOperands(totalOperands);
// Calculate derived metrics
halstead.calculateDerivedMetrics();
metrics.setHalsteadMetrics(halstead);
}
private Set<String> extractOperators(String content) {
Set<String> operators = new HashSet<>();
// Common Java operators
String[] javaOperators = {
"=", "+", "-", "*", "/", "%", "++", "--", "==", "!=", ">", "<", ">=", "<=",
"&&", "||", "!", "&", "|", "^", "~", "<<", ">>", ">>>", "+=", "-=", "*=",
"/=", "%=", "&=", "|=", "^=", "<<=", ">>=", ">>>=", "instanceof"
};
for (String op : javaOperators) {
if (content.contains(op)) {
operators.add(op);
}
}
// Add control flow keywords as operators
String[] controlFlow = {"if", "else", "for", "while", "do", "switch", "case", "break", "continue", "return"};
for (String cf : controlFlow) {
if (content.contains(cf)) {
operators.add(cf);
}
}
return operators;
}
private Set<String> extractOperands(String content) {
Set<String> operands = new HashSet<>();
// Extract identifiers (variable names, method names, etc.)
Pattern identifierPattern = Pattern.compile("\\b[a-zA-Z_$][a-zA-Z0-9_$]*\\b");
Matcher matcher = identifierPattern.matcher(content);
while (matcher.find()) {
String identifier = matcher.group();
// Exclude keywords
if (!isJavaKeyword(identifier)) {
operands.add(identifier);
}
}
return operands;
}
private boolean isJavaKeyword(String word) {
String[] keywords = {
"abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class",
"const", "continue", "default", "do", "double", "else", "enum", "extends", "final",
"finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int",
"interface", "long", "native", "new", "package", "private", "protected", "public",
"return", "short", "static", "strictfp", "super", "switch", "synchronized", "this",
"throw", "throws", "transient", "try", "void", "volatile", "while", "true", "false", "null"
};
return Arrays.asList(keywords).contains(word);
}
private int countTotalOccurrences(String content, Set<String> items) {
int count = 0;
for (String item : items) {
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(item) + "\\b");
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
count++;
}
}
return count;
}
private void calculateMaintainabilityIndex(FileMetrics metrics) {
MetricsResult m = metrics.getMetrics();
HalsteadMetrics h = metrics.getHalsteadMetrics();
// Simplified Maintainability Index calculation
double halsteadVolume = h.getVolume();
double cyclomaticComplexity = m.getCyclomaticComplexity();
double linesOfCode = m.getLinesOfCode();
// Avoid division by zero
if (halsteadVolume == 0) halsteadVolume = 1;
if (linesOfCode == 0) linesOfCode = 1;
// MI = 171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(LOC)
double maintainabilityIndex = 171 - 5.2 * Math.log(halsteadVolume) 
- 0.23 * cyclomaticComplexity 
- 16.2 * Math.log(linesOfCode);
// Clamp between 0 and 100
maintainabilityIndex = Math.max(0, Math.min(100, maintainabilityIndex));
metrics.setMaintainabilityIndex(maintainabilityIndex);
}
private void detectCodeSmells(String content, FileMetrics metrics) {
List<CodeSmell> smells = new ArrayList<>();
// Detect long method
if (metrics.getMetrics().getLinesOfCode() > 50) {
smells.add(new CodeSmell("LONG_METHOD", 
"Method might be too long (" + metrics.getMetrics().getLinesOfCode() + " lines)"));
}
// Detect high complexity
if (metrics.getMetrics().getCyclomaticComplexity() > 10) {
smells.add(new CodeSmell("HIGH_COMPLEXITY", 
"High cyclomatic complexity (" + metrics.getMetrics().getCyclomaticComplexity() + ")"));
}
// Detect many parameters
detectManyParameters(content, smells);
// Detect duplicated code (simplified)
detectDuplicatedCode(content, smells);
metrics.setCodeSmells(smells);
}
private void detectManyParameters(String content, List<CodeSmell> smells) {
Pattern methodPattern = Pattern.compile("\\w+\\s+\\w+\\s*\\(([^)]*)\\)");
Matcher matcher = methodPattern.matcher(content);
while (matcher.find()) {
String params = matcher.group(1);
if (!params.trim().isEmpty()) {
int paramCount = params.split(",").length;
if (paramCount > 5) {
smells.add(new CodeSmell("MANY_PARAMETERS", 
"Method has too many parameters (" + paramCount + ")"));
}
}
}
}
private void detectDuplicatedCode(String content, List<CodeSmell> smells) {
// Simplified duplication detection - look for repeated code blocks
String[] lines = content.split("\n");
Map<String, Integer> lineCounts = new HashMap<>();
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.length() > 20 && !trimmed.startsWith("//")) {
lineCounts.put(trimmed, lineCounts.getOrDefault(trimmed, 0) + 1);
}
}
for (Map.Entry<String, Integer> entry : lineCounts.entrySet()) {
if (entry.getValue() > 3) {
smells.add(new CodeSmell("DUPLICATED_CODE", 
"Possible code duplication (line repeated " + entry.getValue() + " times)"));
break; // Report only one instance to avoid spam
}
}
}
// Getters
public List<File> getJavaFiles() { return javaFiles; }
public List<FileMetrics> getFileMetrics() { return fileMetrics; }
}

Data Model Classes

package com.codemetrics;
import java.util.*;
public class MetricsResult {
private int linesOfCode;
private int blankLines;
private int commentLines;
private int codeLines;
private int methodCount;
private int classCount;
private int interfaceCount;
private int cyclomaticComplexity;
private double averageComplexity;
private int inheritanceDepth;
private int dependencyCount;
public MetricsResult() {}
public void aggregate(MetricsResult other) {
this.linesOfCode += other.linesOfCode;
this.blankLines += other.blankLines;
this.commentLines += other.commentLines;
this.codeLines += other.codeLines;
this.methodCount += other.methodCount;
this.classCount += other.classCount;
this.interfaceCount += other.interfaceCount;
this.cyclomaticComplexity += other.cyclomaticComplexity;
this.inheritanceDepth += other.inheritanceDepth;
this.dependencyCount += other.dependencyCount;
// Recalculate average
if (this.methodCount > 0) {
this.averageComplexity = (double) this.cyclomaticComplexity / this.methodCount;
}
}
// Getters and Setters
public int getLinesOfCode() { return linesOfCode; }
public void setLinesOfCode(int linesOfCode) { this.linesOfCode = linesOfCode; }
public int getBlankLines() { return blankLines; }
public void setBlankLines(int blankLines) { this.blankLines = blankLines; }
public int getCommentLines() { return commentLines; }
public void setCommentLines(int commentLines) { this.commentLines = commentLines; }
public int getCodeLines() { return codeLines; }
public void setCodeLines(int codeLines) { this.codeLines = codeLines; }
public int getMethodCount() { return methodCount; }
public void setMethodCount(int methodCount) { this.methodCount = methodCount; }
public int getClassCount() { return classCount; }
public void setClassCount(int classCount) { this.classCount = classCount; }
public int getInterfaceCount() { return interfaceCount; }
public void setInterfaceCount(int interfaceCount) { this.interfaceCount = interfaceCount; }
public int getCyclomaticComplexity() { return cyclomaticComplexity; }
public void setCyclomaticComplexity(int cyclomaticComplexity) { 
this.cyclomaticComplexity = cyclomaticComplexity; 
}
public double getAverageComplexity() { return averageComplexity; }
public void setAverageComplexity(double averageComplexity) { 
this.averageComplexity = averageComplexity; 
}
public int getInheritanceDepth() { return inheritanceDepth; }
public void setInheritanceDepth(int inheritanceDepth) { this.inheritanceDepth = inheritanceDepth; }
public int getDependencyCount() { return dependencyCount; }
public void setDependencyCount(int dependencyCount) { this.dependencyCount = dependencyCount; }
}
class FileMetrics {
private final String fileName;
private final String filePath;
private final MetricsResult metrics;
private HalsteadMetrics halsteadMetrics;
private double maintainabilityIndex;
private List<CodeSmell> codeSmells;
public FileMetrics(String fileName, String filePath) {
this.fileName = fileName;
this.filePath = filePath;
this.metrics = new MetricsResult();
this.halsteadMetrics = new HalsteadMetrics();
this.codeSmells = new ArrayList<>();
}
public void setLineMetrics(int totalLines, int blankLines, int commentLines, int codeLines) {
metrics.setLinesOfCode(totalLines);
metrics.setBlankLines(blankLines);
metrics.setCommentLines(commentLines);
metrics.setCodeLines(codeLines);
}
// Delegated methods for MetricsResult
public void setMethodCount(int count) { metrics.setMethodCount(count); }
public void setCyclomaticComplexity(int complexity) { metrics.setCyclomaticComplexity(complexity); }
public void setAverageComplexity(double avg) { metrics.setAverageComplexity(avg); }
public void setClassCount(int count) { metrics.setClassCount(count); }
public void setInterfaceCount(int count) { metrics.setInterfaceCount(count); }
public void setInheritanceDepth(int depth) { metrics.setInheritanceDepth(depth); }
public void setDependencyCount(int count) { metrics.setDependencyCount(count); }
// Getters
public String getFileName() { return fileName; }
public String getFilePath() { return filePath; }
public MetricsResult getMetrics() { return metrics; }
public HalsteadMetrics getHalsteadMetrics() { return halsteadMetrics; }
public void setHalsteadMetrics(HalsteadMetrics halsteadMetrics) { this.halsteadMetrics = halsteadMetrics; }
public double getMaintainabilityIndex() { return maintainabilityIndex; }
public void setMaintainabilityIndex(double maintainabilityIndex) { this.maintainabilityIndex = maintainabilityIndex; }
public List<CodeSmell> getCodeSmells() { return codeSmells; }
public void setCodeSmells(List<CodeSmell> codeSmells) { this.codeSmells = codeSmells; }
}
class HalsteadMetrics {
private int uniqueOperators;
private int uniqueOperands;
private int totalOperators;
private int totalOperands;
private double vocabulary;
private double length;
private double volume;
private double difficulty;
private double effort;
public void calculateDerivedMetrics() {
this.vocabulary = uniqueOperators + uniqueOperands;
this.length = totalOperators + totalOperands;
if (vocabulary > 0) {
this.volume = length * (Math.log(vocabulary) / Math.log(2));
}
if (uniqueOperators > 0 && uniqueOperands > 0) {
this.difficulty = ((double) uniqueOperators / 2) * ((double) totalOperands / uniqueOperands);
this.effort = difficulty * volume;
}
}
// Getters and Setters
public int getUniqueOperators() { return uniqueOperators; }
public void setUniqueOperators(int uniqueOperators) { this.uniqueOperators = uniqueOperators; }
public int getUniqueOperands() { return uniqueOperands; }
public void setUniqueOperands(int uniqueOperands) { this.uniqueOperands = uniqueOperands; }
public int getTotalOperators() { return totalOperators; }
public void setTotalOperators(int totalOperators) { this.totalOperators = totalOperators; }
public int getTotalOperands() { return totalOperands; }
public void setTotalOperands(int totalOperands) { this.totalOperands = totalOperands; }
public double getVocabulary() { return vocabulary; }
public double getLength() { return length; }
public double getVolume() { return volume; }
public double getDifficulty() { return difficulty; }
public double getEffort() { return effort; }
}
class CodeSmell {
private final String type;
private final String description;
private final Date detectedAt;
public CodeSmell(String type, String description) {
this.type = type;
this.description = description;
this.detectedAt = new Date();
}
// Getters
public String getType() { return type; }
public String getDescription() { return description; }
public Date getDetectedAt() { return detectedAt; }
@Override
public String toString() {
return type + ": " + description;
}
}
class AnalysisResult {
private final List<FileMetrics> fileMetrics;
private final MetricsResult totalMetrics;
public AnalysisResult(List<FileMetrics> fileMetrics, MetricsResult totalMetrics) {
this.fileMetrics = fileMetrics;
this.totalMetrics = totalMetrics;
}
// Getters
public List<FileMetrics> getFileMetrics() { return fileMetrics; }
public MetricsResult getTotalMetrics() { return totalMetrics; }
}

Report Generators

package com.codemetrics;
import java.util.*;
import java.util.stream.Collectors;
public class MetricsReporter {
public static void generateConsoleReport(AnalysisResult result) {
System.out.println("=== CODE METRICS ANALYSIS REPORT ===");
System.out.println();
generateSummaryReport(result);
System.out.println();
generateDetailedReport(result);
System.out.println();
generateCodeSmellsReport(result);
System.out.println();
generateRecommendations(result);
}
private static void generateSummaryReport(AnalysisResult result) {
MetricsResult total = result.getTotalMetrics();
System.out.println("SUMMARY METRICS");
System.out.println("===============");
System.out.printf("Files Analyzed: %d%n", result.getFileMetrics().size());
System.out.printf("Total Lines: %d%n", total.getLinesOfCode());
System.out.printf("Code Lines: %d (%.1f%%)%n", 
total.getCodeLines(), percentage(total.getCodeLines(), total.getLinesOfCode()));
System.out.printf("Comment Lines: %d (%.1f%%)%n", 
total.getCommentLines(), percentage(total.getCommentLines(), total.getLinesOfCode()));
System.out.printf("Blank Lines: %d (%.1f%%)%n", 
total.getBlankLines(), percentage(total.getBlankLines(), total.getLinesOfCode()));
System.out.printf("Comment Density: %.1f%%%n", 
percentage(total.getCommentLines(), total.getCommentLines() + total.getCodeLines()));
System.out.println();
System.out.printf("Classes: %d%n", total.getClassCount());
System.out.printf("Interfaces: %d%n", total.getInterfaceCount());
System.out.printf("Methods: %d%n", total.getMethodCount());
System.out.printf("Total Cyclomatic Complexity: %d%n", total.getCyclomaticComplexity());
System.out.printf("Average Complexity/Method: %.2f%n", total.getAverageComplexity());
System.out.printf("Average Inheritance Depth: %.2f%n", 
(double) total.getInheritanceDepth() / Math.max(total.getClassCount(), 1));
}
private static void generateDetailedReport(AnalysisResult result) {
System.out.println("DETAILED FILE METRICS");
System.out.println("=====================");
System.out.printf("%-30s %6s %6s %6s %6s %6s %6s %10s%n",
"File", "Lines", "Code", "Comments", "Blanks", "Methods", "Complex", "Maintain");
System.out.printf("%-30s %6s %6s %6s %6s %6s %6s %10s%n",
"----", "-----", "----", "-------", "------", "-------", "-------", "--------");
for (FileMetrics fileMetric : result.getFileMetrics()) {
MetricsResult metrics = fileMetric.getMetrics();
System.out.printf("%-30s %6d %6d %6d %6d %6d %6d %9.1f%n",
truncateFileName(fileMetric.getFileName(), 30),
metrics.getLinesOfCode(),
metrics.getCodeLines(),
metrics.getCommentLines(),
metrics.getBlankLines(),
metrics.getMethodCount(),
metrics.getCyclomaticComplexity(),
fileMetric.getMaintainabilityIndex());
}
}
private static void generateCodeSmellsReport(AnalysisResult result) {
List<CodeSmell> allSmells = result.getFileMetrics().stream()
.flatMap(fm -> fm.getCodeSmells().stream())
.collect(Collectors.toList());
if (allSmells.isEmpty()) {
System.out.println("CODE SMELLS ANALYSIS");
System.out.println("====================");
System.out.println("No significant code smells detected.");
return;
}
System.out.println("CODE SMELLS DETECTED");
System.out.println("====================");
Map<String, Long> smellCounts = allSmells.stream()
.collect(Collectors.groupingBy(CodeSmell::getType, Collectors.counting()));
System.out.println("Summary by Type:");
smellCounts.forEach((type, count) -> 
System.out.printf("  %s: %d occurrences%n", type, count));
System.out.println();
System.out.println("Detailed Findings:");
for (FileMetrics fileMetric : result.getFileMetrics()) {
if (!fileMetric.getCodeSmells().isEmpty()) {
System.out.println("  " + fileMetric.getFileName() + ":");
for (CodeSmell smell : fileMetric.getCodeSmells()) {
System.out.println("    - " + smell.getDescription());
}
}
}
}
private static void generateRecommendations(AnalysisResult result) {
MetricsResult total = result.getTotalMetrics();
List<String> recommendations = new ArrayList<>();
// Complexity recommendations
if (total.getAverageComplexity() > 10) {
recommendations.add("High average complexity detected. Consider refactoring complex methods.");
}
// Comment density recommendations
double commentDensity = percentage(total.getCommentLines(), total.getCommentLines() + total.getCodeLines());
if (commentDensity < 10) {
recommendations.add("Low comment density. Consider adding more documentation.");
} else if (commentDensity > 40) {
recommendations.add("High comment density. Some comments might be redundant.");
}
// Method count recommendations
double avgMethodsPerClass = (double) total.getMethodCount() / Math.max(total.getClassCount(), 1);
if (avgMethodsPerClass > 20) {
recommendations.add("Large classes detected. Consider applying Single Responsibility Principle.");
}
// Maintainability recommendations
double avgMaintainability = result.getFileMetrics().stream()
.mapToDouble(FileMetrics::getMaintainabilityIndex)
.average()
.orElse(0);
if (avgMaintainability < 65) {
recommendations.add("Low maintainability index. Code may be difficult to maintain.");
}
if (!recommendations.isEmpty()) {
System.out.println("RECOMMENDATIONS");
System.out.println("===============");
recommendations.forEach(rec -> System.out.println("• " + rec));
}
}
private static String truncateFileName(String fileName, int maxLength) {
if (fileName.length() <= maxLength) {
return fileName;
}
return "..." + fileName.substring(fileName.length() - maxLength + 3);
}
private static double percentage(int part, int total) {
if (total == 0) return 0;
return (double) part / total * 100;
}
}
// HTML Report Generator
class HTMLReportGenerator {
public static String generateHTMLReport(AnalysisResult result) {
StringBuilder html = new StringBuilder();
html.append("""
<!DOCTYPE html>
<html>
<head>
<title>Code Metrics Analysis Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.header { background: #f0f0f0; padding: 20px; border-radius: 5px; }
.section { margin: 20px 0; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
.good { color: green; }
.warning { color: orange; }
.bad { color: red; }
.metric-card { 
display: inline-block; 
width: 200px; 
margin: 10px; 
padding: 15px; 
border: 1px solid #ddd; 
border-radius: 5px; 
text-align: center;
}
</style>
</head>
<body>
<div class="header">
<h1>Code Metrics Analysis Report</h1>
<p>Generated on: """ + new Date() + """</p>
</div>
""");
generateHTMLSummary(html, result);
generateHTMLDetailedTable(html, result);
generateHTMLCodeSmells(html, result);
html.append("</body></html>");
return html.toString();
}
private static void generateHTMLSummary(StringBuilder html, AnalysisResult result) {
MetricsResult total = result.getTotalMetrics();
html.append("""
<div class="section">
<h2>Summary Metrics</h2>
<div class="metric-card">
<h3>Files Analyzed</h3>
<p style="font-size: 24px; font-weight: bold;">""" + result.getFileMetrics().size() + """</p>
</div>
<div class="metric-card">
<h3>Total Lines</h3>
<p style="font-size: 24px; font-weight: bold;">""" + total.getLinesOfCode() + """</p>
</div>
<div class="metric-card">
<h3>Code Lines</h3>
<p style="font-size: 24px; font-weight: bold;">""" + total.getCodeLines() + """</p>
</div>
<div class="metric-card">
<h3>Methods</h3>
<p style="font-size: 24px; font-weight: bold;">""" + total.getMethodCount() + """</p>
</div>
</div>
""");
}
private static void generateHTMLDetailedTable(StringBuilder html, AnalysisResult result) {
html.append("""
<div class="section">
<h2>Detailed File Metrics</h2>
<table>
<thead>
<tr>
<th>File</th>
<th>Lines</th>
<th>Code</th>
<th>Comments</th>
<th>Blanks</th>
<th>Methods</th>
<th>Complexity</th>
<th>Maintainability</th>
</tr>
</thead>
<tbody>
""");
for (FileMetrics fileMetric : result.getFileMetrics()) {
MetricsResult metrics = fileMetric.getMetrics();
String maintainabilityClass = getMaintainabilityClass(fileMetric.getMaintainabilityIndex());
html.append(String.format("""
<tr>
<td>%s</td>
<td>%d</td>
<td>%d</td>
<td>%d</td>
<td>%d</td>
<td>%d</td>
<td>%d</td>
<td class="%s">%.1f</td>
</tr>
""",
fileMetric.getFileName(),
metrics.getLinesOfCode(),
metrics.getCodeLines(),
metrics.getCommentLines(),
metrics.getBlankLines(),
metrics.getMethodCount(),
metrics.getCyclomaticComplexity(),
maintainabilityClass,
fileMetric.getMaintainabilityIndex()
));
}
html.append("</tbody></table></div>");
}
private static void generateHTMLCodeSmells(StringBuilder html, AnalysisResult result) {
List<CodeSmell> allSmells = result.getFileMetrics().stream()
.flatMap(fm -> fm.getCodeSmells().stream())
.collect(Collectors.toList());
if (!allSmells.isEmpty()) {
html.append("""
<div class="section">
<h2>Code Smells Detected</h2>
<ul>
""");
for (CodeSmell smell : allSmells) {
html.append("<li><strong>").append(smell.getType())
.append(":</strong> ").append(smell.getDescription()).append("</li>");
}
html.append("</ul></div>");
}
}
private static String getMaintainabilityClass(double index) {
if (index >= 85) return "good";
if (index >= 65) return "warning";
return "bad";
}
}

Main Application Class

package com.codemetrics;
import java.io.*;
import java.nio.file.*;
public class CodeMetricsApplication {
public static void main(String[] args) {
if (args.length == 0) {
System.out.println("Usage: java CodeMetricsApplication <directory-path> [--html]");
System.out.println("Example: java CodeMetricsCalculator /path/to/java/source --html");
return;
}
String directoryPath = args[0];
boolean generateHTML = args.length > 1 && "--html".equals(args[1]);
try {
CodeMetricsCalculator calculator = new CodeMetricsCalculator(directoryPath);
AnalysisResult result = calculator.analyze();
// Generate console report
MetricsReporter.generateConsoleReport(result);
// Generate HTML report if requested
if (generateHTML) {
String htmlReport = HTMLReportGenerator.generateHTMLReport(result);
Path htmlPath = Paths.get("code-metrics-report.html");
Files.write(htmlPath, htmlReport.getBytes());
System.out.println("\nHTML report generated: " + htmlPath.toAbsolutePath());
}
} catch (IOException e) {
System.err.println("Error analyzing code: " + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
System.err.println("Unexpected error: " + e.getMessage());
e.printStackTrace();
}
}
}

Usage Examples

Basic Usage

// Analyze a directory
CodeMetricsCalculator calculator = new CodeMetricsCalculator("/path/to/java/project");
AnalysisResult result = calculator.analyze();
// Generate console report
MetricsReporter.generateConsoleReport(result);
// Generate HTML report
String html = HTMLReportGenerator.generateHTMLReport(result);
Files.write(Paths.get("report.html"), html.getBytes());

Programmatic Usage

// Analyze specific files
List<File> javaFiles = Arrays.asList(
new File("src/main/java/com/example/MyClass.java"),
new File("src/main/java/com/example/AnotherClass.java")
);
CodeMetricsCalculator calculator = new CodeMetricsCalculator(javaFiles);
AnalysisResult result = calculator.analyze();
// Access metrics programmatically
for (FileMetrics fileMetric : result.getFileMetrics()) {
System.out.println("File: " + fileMetric.getFileName());
System.out.println("Maintainability: " + fileMetric.getMaintainabilityIndex());
System.out.println("Complexity: " + fileMetric.getMetrics().getCyclomaticComplexity());
}

Features Summary

  1. Comprehensive Metrics: LOC, complexity, Halstead metrics, maintainability index
  2. Code Smell Detection: Identifies common anti-patterns
  3. Multiple Output Formats: Console and HTML reports
  4. Flexible Analysis: Can analyze directories or specific files
  5. Extensible Architecture: Easy to add new metrics and analyzers

This Code Metrics Calculator provides valuable insights into code quality and helps identify areas for improvement in Java projects.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper