Introduction to CodeClimate
CodeClimate is a cloud-based automated code review tool that helps teams monitor code quality, track technical debt, and enforce coding standards. It provides both static analysis (CodeClimate CLI) and test coverage analysis.
Key Features
- Automated Code Review: Continuous analysis of pull requests
- Maintainability Metrics: GPA scoring and technical debt tracking
- Test Coverage: Integration with coverage tools
- Multiple Engines: Supports various analysis tools (Checkstyle, PMD, SpotBugs, etc.)
- GitHub Integration: Seamless PR analysis and comments
Implementation Guide
Dependencies
Add to your pom.xml:
<properties>
<checkstyle.version>10.12.5</checkstyle.version>
<pmd.version>6.55.0</pmd.version>
<spotbugs.version>4.8.2</spotbugs.version>
<jacoco.version>0.8.10</jacoco.version>
</properties>
<dependencies>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Checkstyle -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<configLocation>checkstyle.xml</configLocation>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- PMD -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
<version>3.21.0</version>
<configuration>
<rulesets>
<ruleset>pmd-ruleset.xml</ruleset>
</rulesets>
<printFailingErrors>true</printFailingErrors>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
<goal>cpd-check</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- SpotBugs -->
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.8.2.0</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<xmlOutput>true</xmlOutput>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- JaCoCo for test coverage -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Surefire for testing -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
</configuration>
</plugin>
</plugins>
</build>
Configuration Files
.codeclimate.yml
version: "2" checks: method-lines: enabled: true config: threshold: 25 method-complexity: enabled: true config: threshold: 5 file-lines: enabled: true config: threshold: 250 identical-code: enabled: true config: threshold: 15 plugins: checkstyle: enabled: true channel: "beta" config: file: "checkstyle.xml" pmd: enabled: true channel: "beta" config: file: "pmd-ruleset.xml" spotbugs: enabled: true channel: "beta" jacoco: enabled: true channel: "beta" exclude_patterns: - "**/test/**" - "**/generated/**" - "**/target/**" - "**/build/**" - "**/*Test.java" - "**/*IT.java"
checkstyle.xml
<?xml version="1.0"?> <!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd"> <module name="Checker"> <property name="charset" value="UTF-8"/> <module name="TreeWalker"> <!-- Naming conventions --> <module name="ConstantName"/> <module name="LocalFinalVariableName"/> <module name="LocalVariableName"/> <module name="MemberName"/> <module name="MethodName"/> <module name="PackageName"/> <module name="ParameterName"/> <module name="StaticVariableName"/> <module name="TypeName"/> <!-- Imports --> <module name="AvoidStarImport"/> <module name="IllegalImport"/> <module name="RedundantImport"/> <module name="UnusedImports"/> <!-- Size violations --> <module name="MethodLength"> <property name="max" value="25"/> <property name="tokens" value="METHOD_DEF"/> </module> <module name="ParameterNumber"> <property name="max" value="5"/> </module> <!-- Whitespace --> <module name="EmptyLineSeparator"> <property name="allowNoEmptyLineBetweenFields" value="true"/> </module> <module name="GenericWhitespace"/> <module name="MethodParamPad"/> <module name="NoLineWrap"/> <module name="OperatorWrap"/> <module name="ParenPad"/> <module name="TypecastParenPad"/> <module name="WhitespaceAfter"/> <module name="WhitespaceAround"/> <!-- Modifiers --> <module name="ModifierOrder"/> <module name="RedundantModifier"/> <!-- Coding --> <module name="EmptyBlock"/> <module name="EmptyCatchBlock"/> <module name="LeftCurly"/> <module name="NeedBraces"/> <module name="RightCurly"/> <module name="AvoidInlineConditionals"/> <module name="DoubleBraceInitialization"> <property name="severity" value="warning"/> </module> <module name="OneStatementPerLine"/> <module name="MultipleVariableDeclarations"/> <module name="StringLiteralEquality"/> <!-- Design --> <module name="VisibilityModifier"> <property name="protectedAllowed" value="true"/> </module> <module name="FinalClass"/> <module name="InterfaceIsType"/> <module name="HideUtilityClassConstructor"/> <!-- Metrics --> <module name="CyclomaticComplexity"> <property name="max" value="5"/> </module> <module name="NPathComplexity"> <property name="max" value="50"/> </module> </module> <!-- File length --> <module name="FileLength"> <property name="max" value="500"/> </module> <!-- File tab character --> <module name="FileTabCharacter"> <property name="eachLine" value="true"/> </module> <!-- Suppression --> <module name="SuppressWarningsFilter"/> </module>
pmd-ruleset.xml
<?xml version="1.0"?> <ruleset name="Custom PMD Rules" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd"> <description>Custom PMD rules for CodeClimate</description> <!-- Best Practices --> <rule ref="category/java/bestpractices.xml"> <exclude name="AbstractClassWithoutAbstractMethod"/> <exclude name="AccessorClassGeneration"/> <exclude name="AccessorMethodGeneration"/> <exclude name="AvoidUsingHardCodedIP"/> <exclude name="CheckResultSet"/> <exclude name="ConstantsInInterface"/> <exclude name="DefaultLabelNotLastInSwitchStmt"/> <exclude name="ForLoopCanBeForeach"/> <exclude name="GuardLogStatement"/> <exclude name="JUnit4SuitesShouldUseSuiteAnnotation"/> <exclude name="JUnit4TestShouldUseAfterAnnotation"/> <exclude name="JUnit4TestShouldUseBeforeAnnotation"/> <exclude name="JUnit4TestShouldUseTestAnnotation"/> <exclude name="JUnitAssertionsShouldIncludeMessage"/> <exclude name="JUnitTestContainsTooManyAsserts"/> <exclude name="JUnitTestsShouldIncludeAssert"/> <exclude name="LiteralsFirstInComparisons"/> <exclude name="LooseCoupling"/> <exclude name="MethodReturnsInternalArray"/> <exclude name="MissingOverride"/> <exclude name="OneDeclarationPerLine"/> <exclude name="PositionLiteralsFirstInCaseInsensitiveComparisons"/> <exclude name="PositionLiteralsFirstInComparisons"/> <exclude name="PreserveStackTrace"/> <exclude name="ReplaceHashtableWithMap"/> <exclude name="ReplaceVectorWithList"/> <exclude name="SwitchStmtsShouldHaveDefault"/> <exclude name="SystemPrintln"/> <exclude name="UnusedFormalParameter"/> <exclude name="UnusedLocalVariable"/> <exclude name="UnusedPrivateField"/> <exclude name="UnusedPrivateMethod"/> <exclude name="UseAssertEqualsInsteadOfAssertTrue"/> <exclude name="UseAssertNullInsteadOfAssertTrue"/> <exclude name="UseAssertSameInsteadOfAssertTrue"/> <exclude name="UseAssertTrueInsteadOfAssertEquals"/> <exclude name="UseCollectionIsEmpty"/> <exclude name="UseVarargs"/> </rule> <!-- Code Style --> <rule ref="category/java/codestyle.xml"> <exclude name="AtLeastOneConstructor"/> <exclude name="AvoidFinalLocalVariable"/> <exclude name="AvoidUsingNativeCode"/> <exclude name="CallSuperInConstructor"/> <exclude name="CommentDefaultAccessModifier"/> <exclude name="ConfusingTernary"/> <exclude name="EmptyMethodInAbstractClassShouldBeAbstract"/> <exclude name="ExtendsObject"/> <exclude name="FieldDeclarationsShouldBeAtStartOfClass"/> <exclude name="ForLoopShouldBeWhileLoop"/> <exclude name="GodClass"/> <exclude name="LocalVariableCouldBeFinal"/> <exclude name="LongVariable"/> <exclude name="MDBAndSessionBeanNamingConvention"/> <exclude name="MethodArgumentCouldBeFinal"/> <exclude name="OnlyOneReturn"/> <exclude name="ShortClassName"/> <exclude name="ShortMethodName"/> <exclude name="ShortVariable"/> <exclude name="TooManyStaticImports"/> <exclude name="UnnecessaryConstructor"/> <exclude name="UnnecessaryModifier"/> <exclude name="UnnecessaryReturn"/> <exclude name="UselessParentheses"/> <exclude name="UselessQualifiedThis"/> </rule> <!-- Design --> <rule ref="category/java/design.xml"> <exclude name="AbstractClassWithoutAnyMethod"/> <exclude name="AvoidCatchingGenericException"/> <exclude name="CouplingBetweenObjects"/> <exclude name="CyclomaticComplexity"/> <exclude name="DataClass"/> <exclude name="ExcessiveClassLength"/> <exclude name="ExcessiveImports"/> <exclude name="ExcessiveMethodLength"/> <exclude name="ExcessiveParameterList"/> <exclude name="ExcessivePublicCount"/> <exclude name="FinalFieldCouldBeStatic"/> <exclude name="ImmutableField"/> <exclude name="LawOfDemeter"/> <exclude name="LoosePackageCoupling"/> <exclude name="NcssCount"/> <exclude name="NPathComplexity"/> <exclude name="TooManyFields"/> <exclude name="TooManyMethods"/> <exclude name="UseObjectForClearerAPI"/> </rule> <!-- Error Prone --> <rule ref="category/java/errorprone.xml"> <exclude name="AssignmentInOperand"/> <exclude name="AvoidAccessibilityAlteration"/> <exclude name="AvoidAssertAsIdentifier"/> <exclude name="AvoidBranchingStatementAsLastInLoop"/> <exclude name="AvoidCatchingNPE"/> <exclude name="AvoidCatchingThrowable"/> <exclude name="AvoidDecimalLiteralsInBigDecimalConstructor"/> <exclude name="AvoidDuplicateLiterals"/> <exclude name="AvoidEnumAsIdentifier"/> <exclude name="AvoidFieldNameMatchingMethodName"/> <exclude name="AvoidFieldNameMatchingTypeName"/> <exclude name="AvoidInstanceofChecksInCatchClause"/> <exclude name="AvoidLiteralsInIfCondition"/> <exclude name="AvoidLosingExceptionInformation"/> <exclude name="AvoidMultipleUnaryOperators"/> <exclude name="AvoidUsingOctalValues"/> <exclude name="BeanMembersShouldSerialize"/> <exclude name="BrokenNullCheck"/> <exclude name="CheckSkipResult"/> <exclude name="ClassCastExceptionWithToArray"/> <exclude name="CloneMethodMustBePublic"/> <exclude name="CloneMethodMustImplementCloneable"/> <exclude name="CloneMethodReturnTypeMustMatchClassName"/> <exclude name="CloneThrowsCloneNotSupportedException"/> <exclude name="CloseResource"/> <exclude name="CompareObjectsWithEquals"/> <exclude name="ConstructorCallsOverridableMethod"/> <exclude name="DoNotCallGarbageCollectionExplicitly"/> <exclude name="DoNotCallSystemExit"/> <exclude name="DoNotExtendJavaLangError"/> <exclude name="DoNotThrowExceptionInFinally"/> <exclude name="DontImportSun"/> <exclude name="DontUseFloatTypeForLoopIndices"/> <exclude name="EmptyCatchBlock"/> <exclude name="EmptyFinalizer"/> <exclude name="EmptyIfStmt"/> <exclude name="EmptyInitializer"/> <exclude name="EmptyStatementBlock"/> <exclude name="EmptyStatementNotInLoop"/> <exclude name="EmptySwitchStatements"/> <exclude name="EmptySynchronizedBlock"/> <exclude name="EmptyTryBlock"/> <exclude name="EmptyWhileStmt"/> <exclude name="EqualsNull"/> <exclude name="FinalizeDoesNotCallSuperFinalize"/> <exclude name="FinalizeOnlyCallsSuperFinalize"/> <exclude name="FinalizeOverloaded"/> <exclude name="FinalizeShouldBeProtected"/> <exclude name="IdempotentOperations"/> <exclude name="ImportFromSamePackage"/> <exclude name="InstantiationToGetClass"/> <exclude name="InvalidLogMessageFormat"/> <exclude name="JumbledIncrementer"/> <exclude name="JUnitSpelling"/> <exclude name="JUnitStaticSuite"/> <exclude name="LoggerIsNotStaticFinal"/> <exclude name="MethodWithSameNameAsEnclosingClass"/> <exclude name="MisplacedNullCheck"/> <exclude name="MissingBreakInSwitch"/> <exclude name="MissingStaticMethodInNonInstantiatableClass"/> <exclude name="MoreThanOneLogger"/> <exclude name="NonCaseLabelInSwitchStatement"/> <exclude name="NonStaticInitializer"/> <exclude name="NullAssignment"/> <exclude name="OverrideBothEqualsAndHashcode"/> <exclude name="ProperCloneImplementation"/> <exclude name="ProperLogger"/> <exclude name="ReturnEmptyArrayRatherThanNull"/> <exclude name="ReturnFromFinallyBlock"/> <exclude name="SimpleDateFormatNeedsLocale"/> <exclude name="SingleMethodSingleton"/> <exclude name="SingletonClassReturningNewInstance"/> <exclude name="StaticEJBFieldShouldBeFinal"/> <exclude name="StringBufferInstantiationWithChar"/> <exclude name="SuspiciousEqualsMethodName"/> <exclude name="SuspiciousHashcodeMethodName"/> <exclude name="SuspiciousOctalEscape"/> <exclude name="TestClassWithoutTestCases"/> <exclude name="UnconditionalIfStatement"/> <exclude name="UnnecessaryBooleanAssertion"/> <exclude name="UnnecessaryCaseChange"/> <exclude name="UnnecessaryConversionTemporary"/> <exclude name="UnusedNullCheckInEquals"/> <exclude name="UseCorrectExceptionLogging"/> <exclude name="UseEqualsToCompareStrings"/> <exclude name="UselessOperationOnImmutable"/> <exclude name="UseLocaleWithCaseConversions"/> <exclude name="UseProperClassLoader"/> </rule> <!-- Performance --> <rule ref="category/java/performance.xml"> <exclude name="AddEmptyString"/> <exclude name="AppendCharacterWithChar"/> <exclude name="AvoidArrayLoops"/> <exclude name="AvoidInstantiatingObjectsInLoops"/> <exclude name="AvoidUsingShortType"/> <exclude name="BigIntegerInstantiation"/> <exclude name="BooleanInstantiation"/> <exclude name="ByteInstantiation"/> <exclude name="ConsecutiveAppendsShouldReuse"/> <exclude name="ConsecutiveLiteralAppends"/> <exclude name="InefficientEmptyStringCheck"/> <exclude name="InefficientStringBuffering"/> <exclude name="InsufficientStringBufferDeclaration"/> <exclude name="IntegerInstantiation"/> <exclude name="LongInstantiation"/> <exclude name="OptimizableToArrayCall"/> <exclude name="RedundantFieldInitializer"/> <exclude name="ShortInstantiation"/> <exclude name="SimplifyStartsWith"/> <exclude name="StringInstantiation"/> <exclude name="StringToString"/> <exclude name="TooFewBranchesForASwitchStatement"/> <exclude name="UseArrayListInsteadOfVector"/> <exclude name="UseArraysAsList"/> <exclude name="UseIndexOfChar"/> <exclude name="UselessStringValueOf"/> <exclude name="UseStringBufferForStringAppends"/> <exclude name="UseStringBufferLength"/> </rule> </ruleset>
Quality Metrics Implementation
Code Quality Service
package com.example.codequality;
import java.io.File;
import java.nio.file.Path;
import java.util.*;
public class CodeQualityService {
private final List<QualityMetric> metrics;
private final QualityThreshold threshold;
public CodeQualityService(QualityThreshold threshold) {
this.threshold = threshold;
this.metrics = Arrays.asList(
new CyclomaticComplexityMetric(),
new MaintainabilityIndexMetric(),
new CodeDuplicationMetric(),
new TestCoverageMetric(),
new CodeSmellMetric()
);
}
public QualityReport analyzeProject(Path projectRoot) {
QualityReport report = new QualityReport();
// Collect all Java files
List<File> javaFiles = collectJavaFiles(projectRoot);
// Calculate metrics
for (QualityMetric metric : metrics) {
MetricResult result = metric.calculate(javaFiles);
report.addMetric(result);
}
// Calculate overall score
report.setOverallScore(calculateOverallScore(report));
report.setStatus(determineStatus(report));
return report;
}
private List<File> collectJavaFiles(Path projectRoot) {
List<File> javaFiles = new ArrayList<>();
collectJavaFilesRecursive(projectRoot.toFile(), javaFiles);
return javaFiles;
}
private void collectJavaFilesRecursive(File directory, List<File> javaFiles) {
File[] files = directory.listFiles();
if (files == null) return;
for (File file : files) {
if (file.isDirectory()) {
// Skip test directories and build outputs
if (!file.getName().equals("test") &&
!file.getName().equals("target") &&
!file.getName().equals("build")) {
collectJavaFilesRecursive(file, javaFiles);
}
} else if (file.getName().endsWith(".java")) {
javaFiles.add(file);
}
}
}
private double calculateOverallScore(QualityReport report) {
double totalScore = 0.0;
int metricCount = 0;
for (MetricResult metric : report.getMetrics()) {
totalScore += metric.getScore();
metricCount++;
}
return metricCount > 0 ? totalScore / metricCount : 0.0;
}
private QualityStatus determineStatus(QualityReport report) {
double overallScore = report.getOverallScore();
if (overallScore >= threshold.getExcellentThreshold()) {
return QualityStatus.EXCELLENT;
} else if (overallScore >= threshold.getGoodThreshold()) {
return QualityStatus.GOOD;
} else if (overallScore >= threshold.getFairThreshold()) {
return QualityStatus.FAIR;
} else {
return QualityStatus.POOR;
}
}
}
Quality Metrics
package com.example.codequality.metrics;
import java.io.File;
import java.util.List;
public interface QualityMetric {
String getName();
String getDescription();
MetricResult calculate(List<File> javaFiles);
double getWeight();
}
public class CyclomaticComplexityMetric implements QualityMetric {
@Override
public String getName() {
return "Cyclomatic Complexity";
}
@Override
public String getDescription() {
return "Measures the complexity of methods based on decision points";
}
@Override
public MetricResult calculate(List<File> javaFiles) {
double totalComplexity = 0;
int methodCount = 0;
int highComplexityMethods = 0;
// Simplified calculation - in practice, use a library like Checkstyle
for (File file : javaFiles) {
FileAnalysisResult analysis = analyzeFileComplexity(file);
totalComplexity += analysis.getTotalComplexity();
methodCount += analysis.getMethodCount();
highComplexityMethods += analysis.getHighComplexityMethods();
}
double averageComplexity = methodCount > 0 ? totalComplexity / methodCount : 0;
double highComplexityRatio = methodCount > 0 ? (double) highComplexityMethods / methodCount : 0;
// Score based on complexity thresholds
double score = calculateComplexityScore(averageComplexity, highComplexityRatio);
return new MetricResult(getName(), score, averageComplexity,
String.format("Average complexity: %.2f, High complexity methods: %d/%d (%.1f%%)",
averageComplexity, highComplexityMethods, methodCount, highComplexityRatio * 100));
}
@Override
public double getWeight() {
return 0.25; // 25% weight in overall score
}
private double calculateComplexityScore(double averageComplexity, double highComplexityRatio) {
if (averageComplexity <= 5 && highComplexityRatio <= 0.05) {
return 1.0; // Excellent
} else if (averageComplexity <= 10 && highComplexityRatio <= 0.1) {
return 0.8; // Good
} else if (averageComplexity <= 15 && highComplexityRatio <= 0.2) {
return 0.6; // Fair
} else {
return 0.4; // Poor
}
}
private FileAnalysisResult analyzeFileComplexity(File file) {
// Simplified implementation
// In practice, use a proper complexity analysis tool
return new FileAnalysisResult(10, 5, 1); // Example values
}
}
public class MaintainabilityIndexMetric implements QualityMetric {
@Override
public String getName() {
return "Maintainability Index";
}
@Override
public String getDescription() {
return "Measures how maintainable the code is based on various factors";
}
@Override
public MetricResult calculate(List<File> javaFiles) {
double totalMaintainability = 0;
int fileCount = javaFiles.size();
for (File file : javaFiles) {
double fileMaintainability = calculateFileMaintainability(file);
totalMaintainability += fileMaintainability;
}
double averageMaintainability = fileCount > 0 ? totalMaintainability / fileCount : 0;
double score = calculateMaintainabilityScore(averageMaintainability);
return new MetricResult(getName(), score, averageMaintainability,
String.format("Average maintainability index: %.2f", averageMaintainability));
}
@Override
public double getWeight() {
return 0.30; // 30% weight in overall score
}
private double calculateMaintainabilityScore(double maintainability) {
if (maintainability >= 85) return 1.0;
else if (maintainability >= 65) return 0.8;
else if (maintainability >= 45) return 0.6;
else return 0.4;
}
private double calculateFileMaintainability(File file) {
// Simplified calculation
// In practice, use Halstead volume, cyclomatic complexity, and lines of code
return 75.0; // Example value
}
}
public class CodeDuplicationMetric implements QualityMetric {
@Override
public String getName() {
return "Code Duplication";
}
@Override
public String getDescription() {
return "Measures the percentage of duplicated code";
}
@Override
public MetricResult calculate(List<File> javaFiles) {
int totalLines = 0;
int duplicatedLines = 0;
for (File file : javaFiles) {
FileDuplicationAnalysis analysis = analyzeFileDuplication(file);
totalLines += analysis.getTotalLines();
duplicatedLines += analysis.getDuplicatedLines();
}
double duplicationRatio = totalLines > 0 ? (double) duplicatedLines / totalLines : 0;
double score = calculateDuplicationScore(duplicationRatio);
return new MetricResult(getName(), score, duplicationRatio * 100,
String.format("Duplication: %.1f%% (%d/%d lines)",
duplicationRatio * 100, duplicatedLines, totalLines));
}
@Override
public double getWeight() {
return 0.20; // 20% weight in overall score
}
private double calculateDuplicationScore(double duplicationRatio) {
if (duplicationRatio <= 0.03) return 1.0; // 3% or less
else if (duplicationRatio <= 0.08) return 0.8; // 8% or less
else if (duplicationRatio <= 0.15) return 0.6; // 15% or less
else return 0.4; // More than 15%
}
private FileDuplicationAnalysis analyzeFileDuplication(File file) {
// Simplified implementation
return new FileDuplicationAnalysis(100, 5); // Example values
}
}
Data Models
package com.example.codequality;
import java.util.ArrayList;
import java.util.List;
public class QualityReport {
private double overallScore;
private QualityStatus status;
private List<MetricResult> metrics;
private String projectName;
private long analysisTimestamp;
public QualityReport() {
this.metrics = new ArrayList<>();
this.analysisTimestamp = System.currentTimeMillis();
}
// Getters and setters
public double getOverallScore() { return overallScore; }
public void setOverallScore(double overallScore) { this.overallScore = overallScore; }
public QualityStatus getStatus() { return status; }
public void setStatus(QualityStatus status) { this.status = status; }
public List<MetricResult> getMetrics() { return metrics; }
public void addMetric(MetricResult metric) { this.metrics.add(metric); }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public long getAnalysisTimestamp() { return analysisTimestamp; }
}
public class MetricResult {
private final String name;
private final double score; // 0.0 to 1.0
private final double value;
private final String description;
public MetricResult(String name, double score, double value, String description) {
this.name = name;
this.score = score;
this.value = value;
this.description = description;
}
// Getters
public String getName() { return name; }
public double getScore() { return score; }
public double getValue() { return value; }
public String getDescription() { return description; }
}
public enum QualityStatus {
EXCELLENT("Excellent", "#4CAF50"),
GOOD("Good", "#8BC34A"),
FAIR("Fair", "#FFC107"),
POOR("Poor", "#F44336");
private final String displayName;
private final String color;
QualityStatus(String displayName, String color) {
this.displayName = displayName;
this.color = color;
}
public String getDisplayName() { return displayName; }
public String getColor() { return color; }
}
public class QualityThreshold {
private double excellentThreshold;
private double goodThreshold;
private double fairThreshold;
public QualityThreshold() {
this.excellentThreshold = 0.9;
this.goodThreshold = 0.7;
this.fairThreshold = 0.5;
}
// Getters and setters
public double getExcellentThreshold() { return excellentThreshold; }
public void setExcellentThreshold(double excellentThreshold) {
this.excellentThreshold = excellentThreshold;
}
public double getGoodThreshold() { return goodThreshold; }
public void setGoodThreshold(double goodThreshold) {
this.goodThreshold = goodThreshold;
}
public double getFairThreshold() { return fairThreshold; }
public void setFairThreshold(double fairThreshold) {
this.fairThreshold = fairThreshold;
}
}
GitHub Actions Integration
.github/workflows/codeclimate.yml
name: CodeClimate Analysis
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
codeclimate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Maven packages
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Run tests with coverage
run: mvn -B test jacoco:report
- name: Run static analysis
run: |
mvn -B checkstyle:checkstyle pmd:check spotbugs:check
mvn -B checkstyle:check pmd:check spotbugs:check
- name: Upload coverage to CodeClimate
uses: codecov/codecov-action@v3
with:
file: ./target/site/jacoco/jacoco.xml
flags: unittests
name: codecov-umbrella
- name: Run CodeClimate
uses: paambaati/[email protected]
env:
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
with:
coverageLocations: |
target/site/jacoco/jacoco.xml:jacoco
Custom CodeClimate Engine
Engine Definition
package com.example.codeclimate.engine;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.*;
import java.nio.file.*;
import java.util.*;
public class CustomCodeClimateEngine {
private static final ObjectMapper mapper = new ObjectMapper();
public static void main(String[] args) {
if (args.length < 1) {
System.err.println("Usage: java CustomCodeClimateEngine <analysis-root>");
System.exit(1);
}
Path analysisRoot = Paths.get(args[0]);
try {
List<Issue> issues = analyzeCode(analysisRoot);
outputIssues(issues);
} catch (Exception e) {
System.err.println("Analysis failed: " + e.getMessage());
System.exit(2);
}
}
private static List<Issue> analyzeCode(Path root) throws IOException {
List<Issue> issues = new ArrayList<>();
// Collect Java files
List<Path> javaFiles = Files.walk(root)
.filter(path -> path.toString().endsWith(".java"))
.filter(path -> !path.toString().contains("/test/"))
.toList();
// Run various analyzers
issues.addAll(analyzeComplexity(javaFiles));
issues.addAll(analyzeDuplication(javaFiles));
issues.addAll(analyzeCodeSmells(javaFiles));
issues.addAll(analyzeSecurity(javaFiles));
return issues;
}
private static List<Issue> analyzeComplexity(List<Path> javaFiles) throws IOException {
List<Issue> issues = new ArrayList<>();
for (Path file : javaFiles) {
List<String> lines = Files.readAllLines(file);
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
if (line.contains("if (") || line.contains("for (") || line.contains("while (")) {
// Simple complexity detection
issues.add(new Issue(
file.toString(),
i + 1,
"High cyclomatic complexity",
"This method has high complexity. Consider refactoring.",
"complexity",
"major"
));
}
}
}
return issues;
}
private static List<Issue> analyzeDuplication(List<Path> javaFiles) throws IOException {
List<Issue> issues = new ArrayList<>();
Map<String, List<Path>> codeBlocks = new HashMap<>();
for (Path file : javaFiles) {
List<String> lines = Files.readAllLines(file);
for (int i = 0; i < lines.size() - 5; i++) {
StringBuilder block = new StringBuilder();
for (int j = 0; j < 5; j++) {
block.append(lines.get(i + j).trim());
}
String blockHash = block.toString().hashCode() + "";
codeBlocks.computeIfAbsent(blockHash, k -> new ArrayList<>())
.add(file);
}
}
// Report duplication
for (List<Path> duplicates : codeBlocks.values()) {
if (duplicates.size() > 1) {
for (Path file : duplicates) {
issues.add(new Issue(
file.toString(),
1,
"Duplicate code detected",
"This code appears in multiple places. Consider extracting to a common method.",
"duplication",
"minor"
));
}
}
}
return issues;
}
private static List<Issue> analyzeCodeSmells(List<Path> javaFiles) throws IOException {
List<Issue> issues = new ArrayList<>();
for (Path file : javaFiles) {
List<String> lines = Files.readAllLines(file);
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
// Long method detection
if (line.contains("public") && line.contains("(") && line.contains(")") &&
line.contains("{")) {
// Check method length (simplified)
int methodStart = i;
int braceCount = 1;
int j = i + 1;
while (j < lines.size() && braceCount > 0) {
String currentLine = lines.get(j);
braceCount += countOccurrences(currentLine, '{');
braceCount -= countOccurrences(currentLine, '}');
j++;
}
int methodLength = j - methodStart;
if (methodLength > 25) {
issues.add(new Issue(
file.toString(),
methodStart + 1,
"Long method",
String.format("Method has %d lines. Consider breaking it into smaller methods.", methodLength),
"style",
"info"
));
}
}
// Magic number detection
if (line.matches(".*\\b\\d{3,}\\b.*") &&
!line.contains("100") && !line.contains("1000") && !line.contains("1024")) {
issues.add(new Issue(
file.toString(),
i + 1,
"Magic number",
"Replace magic number with named constant.",
"style",
"minor"
));
}
}
}
return issues;
}
private static List<Issue> analyzeSecurity(List<Path> javaFiles) throws IOException {
List<Issue> issues = new ArrayList<>();
for (Path file : javaFiles) {
List<String> lines = Files.readAllLines(file);
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
// SQL injection detection
if (line.contains("createStatement") || line.contains("executeQuery")) {
if (line.contains("+") && line.contains("\"")) {
issues.add(new Issue(
file.toString(),
i + 1,
"Potential SQL injection",
"Use prepared statements instead of string concatenation.",
"security",
"critical"
));
}
}
// Hard-coded password detection
if (line.toLowerCase().contains("password") && line.contains("=") &&
line.contains("\"")) {
issues.add(new Issue(
file.toString(),
i + 1,
"Hard-coded password",
"Store passwords in secure configuration, not in source code.",
"security",
"critical"
));
}
}
}
return issues;
}
private static int countOccurrences(String str, char ch) {
int count = 0;
for (char c : str.toCharArray()) {
if (c == ch) count++;
}
return count;
}
private static void outputIssues(List<Issue> issues) throws IOException {
for (Issue issue : issues) {
mapper.writeValue(System.out, issue);
System.out.println(); // New line after each issue
}
}
public static class Issue {
public final String type = "issue";
public final Check check_name;
public final String description;
public final List<Location> locations;
public final Severity severity;
public Issue(String filePath, int line, String checkName, String description,
String category, String severity) {
this.check_name = new Check(checkName, category);
this.description = description;
this.locations = List.of(new Location(filePath, line));
this.severity = new Severity(severity);
}
}
public static class Check {
public final String name;
public final String category;
public Check(String name, String category) {
this.name = name;
this.category = category;
}
}
public static class Location {
public final Path path;
public final Lines lines;
public Location(String filePath, int line) {
this.path = Paths.get(filePath);
this.lines = new Lines(line);
}
}
public static class Lines {
public final int begin;
public Lines(int begin) {
this.begin = begin;
}
}
public static class Severity {
public final String type;
public Severity(String type) {
this.type = type;
}
}
}
Test Coverage Analysis
Coverage Service
package com.example.codequality.coverage;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.util.*;
public class CoverageAnalyzer {
public CoverageReport analyzeJacocoReport(File jacocoXmlReport) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(jacocoXmlReport);
Element root = document.getDocumentElement();
NodeList packageNodes = root.getElementsByTagName("package");
List<PackageCoverage> packageCoverages = new ArrayList<>();
double totalLineCoverage = 0;
double totalBranchCoverage = 0;
int packageCount = 0;
for (int i = 0; i < packageNodes.getLength(); i++) {
Element packageElement = (Element) packageNodes.item(i);
PackageCoverage packageCoverage = analyzePackage(packageElement);
packageCoverages.add(packageCoverage);
totalLineCoverage += packageCoverage.getLineCoverage();
totalBranchCoverage += packageCoverage.getBranchCoverage();
packageCount++;
}
double overallLineCoverage = packageCount > 0 ? totalLineCoverage / packageCount : 0;
double overallBranchCoverage = packageCount > 0 ? totalBranchCoverage / packageCount : 0;
return new CoverageReport(overallLineCoverage, overallBranchCoverage, packageCoverages);
} catch (Exception e) {
throw new RuntimeException("Failed to analyze JaCoCo report", e);
}
}
private PackageCoverage analyzePackage(Element packageElement) {
String packageName = packageElement.getAttribute("name");
NodeList classNodes = packageElement.getElementsByTagName("class");
List<ClassCoverage> classCoverages = new ArrayList<>();
double totalLineCoverage = 0;
double totalBranchCoverage = 0;
int classCount = 0;
for (int i = 0; i < classNodes.getLength(); i++) {
Element classElement = (Element) classNodes.item(i);
ClassCoverage classCoverage = analyzeClass(classElement);
classCoverages.add(classCoverage);
totalLineCoverage += classCoverage.getLineCoverage();
totalBranchCoverage += classCoverage.getBranchCoverage();
classCount++;
}
double packageLineCoverage = classCount > 0 ? totalLineCoverage / classCount : 0;
double packageBranchCoverage = classCount > 0 ? totalBranchCoverage / classCount : 0;
return new PackageCoverage(packageName, packageLineCoverage, packageBranchCoverage, classCoverages);
}
private ClassCoverage analyzeClass(Element classElement) {
String className = classElement.getAttribute("name");
Element counterElement = findCounter(classElement, "LINE");
long coveredLines = Long.parseLong(counterElement.getAttribute("covered"));
long missedLines = Long.parseLong(counterElement.getAttribute("missed"));
double lineCoverage = calculateCoverage(coveredLines, missedLines);
counterElement = findCounter(classElement, "BRANCH");
long coveredBranches = Long.parseLong(counterElement.getAttribute("covered"));
long missedBranches = Long.parseLong(counterElement.getAttribute("missed"));
double branchCoverage = calculateCoverage(coveredBranches, missedBranches);
return new ClassCoverage(className, lineCoverage, branchCoverage);
}
private Element findCounter(Element parent, String type) {
NodeList counterNodes = parent.getElementsByTagName("counter");
for (int i = 0; i < counterNodes.getLength(); i++) {
Element counter = (Element) counterNodes.item(i);
if (type.equals(counter.getAttribute("type"))) {
return counter;
}
}
// Return empty counter if not found
Element emptyCounter = parent.getOwnerDocument().createElement("counter");
emptyCounter.setAttribute("covered", "0");
emptyCounter.setAttribute("missed", "0");
return emptyCounter;
}
private double calculateCoverage(long covered, long missed) {
long total = covered + missed;
return total > 0 ? (double) covered / total * 100 : 0;
}
}
// Coverage data models
class CoverageReport {
private final double overallLineCoverage;
private final double overallBranchCoverage;
private final List<PackageCoverage> packages;
private final Date analysisDate;
public CoverageReport(double overallLineCoverage, double overallBranchCoverage,
List<PackageCoverage> packages) {
this.overallLineCoverage = overallLineCoverage;
this.overallBranchCoverage = overallBranchCoverage;
this.packages = packages;
this.analysisDate = new Date();
}
// Getters
public double getOverallLineCoverage() { return overallLineCoverage; }
public double getOverallBranchCoverage() { return overallBranchCoverage; }
public List<PackageCoverage> getPackages() { return packages; }
public Date getAnalysisDate() { return analysisDate; }
public CoverageQuality getQuality() {
if (overallLineCoverage >= 80) return CoverageQuality.EXCELLENT;
else if (overallLineCoverage >= 70) return CoverageQuality.GOOD;
else if (overallLineCoverage >= 50) return CoverageQuality.FAIR;
else return CoverageQuality.POOR;
}
}
class PackageCoverage {
private final String packageName;
private final double lineCoverage;
private final double branchCoverage;
private final List<ClassCoverage> classes;
public PackageCoverage(String packageName, double lineCoverage, double branchCoverage,
List<ClassCoverage> classes) {
this.packageName = packageName;
this.lineCoverage = lineCoverage;
this.branchCoverage = branchCoverage;
this.classes = classes;
}
// Getters
public String getPackageName() { return packageName; }
public double getLineCoverage() { return lineCoverage; }
public double getBranchCoverage() { return branchCoverage; }
public List<ClassCoverage> getClasses() { return classes; }
}
class ClassCoverage {
private final String className;
private final double lineCoverage;
private final double branchCoverage;
public ClassCoverage(String className, double lineCoverage, double branchCoverage) {
this.className = className;
this.lineCoverage = lineCoverage;
this.branchCoverage = branchCoverage;
}
// Getters
public String getClassName() { return className; }
public double getLineCoverage() { return lineCoverage; }
public double getBranchCoverage() { return branchCoverage; }
}
enum CoverageQuality {
EXCELLENT("Excellent", 80, 100),
GOOD("Good", 70, 79),
FAIR("Fair", 50, 69),
POOR("Poor", 0, 49);
private final String displayName;
private final int minCoverage;
private final int maxCoverage;
CoverageQuality(String displayName, int minCoverage, int maxCoverage) {
this.displayName = displayName;
this.minCoverage = minCoverage;
this.maxCoverage = maxCoverage;
}
public String getDisplayName() { return displayName; }
public int getMinCoverage() { return minCoverage; }
public int getMaxCoverage() { return maxCoverage; }
}
Best Practices and Usage
Quality Gates
package com.example.codequality.gates;
import com.example.codequality.CoverageReport;
import com.example.codequality.QualityReport;
public class QualityGate {
private final double minCoverage;
private final double minQualityScore;
private final int maxCriticalIssues;
private final int maxMajorIssues;
public QualityGate(double minCoverage, double minQualityScore,
int maxCriticalIssues, int maxMajorIssues) {
this.minCoverage = minCoverage;
this.minQualityScore = minQualityScore;
this.maxCriticalIssues = maxCriticalIssues;
this.maxMajorIssues = maxMajorIssues;
}
public QualityGateResult evaluate(QualityReport qualityReport, CoverageReport coverageReport) {
List<String> failures = new ArrayList<>();
// Check coverage
if (coverageReport.getOverallLineCoverage() < minCoverage) {
failures.add(String.format(
"Coverage %.1f%% is below minimum required %.1f%%",
coverageReport.getOverallLineCoverage(), minCoverage
));
}
// Check quality score
if (qualityReport.getOverallScore() < minQualityScore) {
failures.add(String.format(
"Quality score %.2f is below minimum required %.2f",
qualityReport.getOverallScore(), minQualityScore
));
}
// Check critical issues (simplified)
// In practice, you'd count actual issues from analysis tools
boolean passed = failures.isEmpty();
return new QualityGateResult(passed, failures);
}
}
class QualityGateResult {
private final boolean passed;
private final List<String> failureReasons;
public QualityGateResult(boolean passed, List<String> failureReasons) {
this.passed = passed;
this.failureReasons = failureReasons;
}
public boolean isPassed() { return passed; }
public List<String> getFailureReasons() { return failureReasons; }
}
Usage Example
package com.example.codequality;
import java.nio.file.Paths;
public class CodeClimateExample {
public static void main(String[] args) {
// Configure quality thresholds
QualityThreshold threshold = new QualityThreshold();
threshold.setExcellentThreshold(0.85);
threshold.setGoodThreshold(0.70);
threshold.setFairThreshold(0.50);
// Create quality service
CodeQualityService qualityService = new CodeQualityService(threshold);
// Analyze project
QualityReport qualityReport = qualityService.analyzeProject(
Paths.get("/path/to/your/project")
);
// Print results
System.out.println("=== CODE QUALITY REPORT ===");
System.out.printf("Overall Score: %.2f%n", qualityReport.getOverallScore());
System.out.printf("Status: %s%n", qualityReport.getStatus().getDisplayName());
System.out.println();
System.out.println("=== METRICS ===");
for (MetricResult metric : qualityReport.getMetrics()) {
System.out.printf("%s: %.2f - %s%n",
metric.getName(), metric.getScore(), metric.getDescription());
}
// Analyze test coverage
CoverageAnalyzer coverageAnalyzer = new CoverageAnalyzer();
CoverageReport coverageReport = coverageAnalyzer.analyzeJacocoReport(
new File("target/site/jacoco/jacoco.xml")
);
System.out.println();
System.out.println("=== TEST COVERAGE ===");
System.out.printf("Line Coverage: %.1f%%%n", coverageReport.getOverallLineCoverage());
System.out.printf("Branch Coverage: %.1f%%%n", coverageReport.getOverallBranchCoverage());
System.out.printf("Quality: %s%n", coverageReport.getQuality().getDisplayName());
// Check quality gate
QualityGate qualityGate = new QualityGate(70.0, 0.7, 0, 10);
QualityGateResult gateResult = qualityGate.evaluate(qualityReport, coverageReport);
System.out.println();
System.out.println("=== QUALITY GATE ===");
System.out.printf("Status: %s%n", gateResult.isPassed() ? "PASSED" : "FAILED");
if (!gateResult.isPassed()) {
System.out.println("Failure reasons:");
for (String reason : gateResult.getFailureReasons()) {
System.out.println(" - " + reason);
}
}
}
}
Conclusion
This comprehensive CodeClimate implementation for Java provides:
- Static code analysis integration with Checkstyle, PMD, and SpotBugs
- Test coverage analysis with JaCoCo integration
- Custom quality metrics for maintainability and complexity
- GitHub Actions integration for CI/CD pipelines
- Quality gates for enforcing standards
- Custom analysis engines for specific needs
Key benefits include:
- Automated Code Review - Continuous feedback on code quality
- Technical Debt Tracking - Monitor and manage code maintainability
- Security Vulnerability Detection - Early identification of security issues
- Test Coverage Monitoring - Ensure adequate test coverage
- Integration with Development Workflow - Seamless PR analysis and feedback
The implementation follows industry best practices and provides a solid foundation for maintaining high code quality in Java projects.