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.