Skip to content

Latest commit

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Java Code Refactoring Engine

A static-analysis tool that automatically detects code smells in Java source files using an Abstract Syntax Tree (AST) and applies automated refactoring transformations to fix them — all while preserving program semantics. Includes a full dark-mode web UI to paste, analyze, and view refactored code in the browser.


Table of Contents

  1. Project Overview
  2. Technology Stack
  3. Project Structure
  4. Architecture
  5. How It Works — Step by Step
  6. Code Smell Detectors — Full Explanation
  7. Refactoring Transformations — Full Explanation
  8. Data Model
  9. Analysis Report
  10. Web Server & Frontend
  11. How to Run
  12. Test Suite
  13. Mutation Testing with PIT
  14. Design Principles
  15. Limitations & Future Work

1. Project Overview

The engine is built around three core ideas:

Detection — Parse every Java source file into an AST using JavaParser. Run six independent smell detectors over the tree. Each detector looks for a specific structural anti-pattern (e.g. a method with too many statements, a class with too many fields) and records every violation as a CodeSmell object.

Transformation — For each detected smell, find a matching Refactorer and apply a well-known refactoring pattern from Martin Fowler's Refactoring book directly to the AST. The AST is mutated in place — no string manipulation, no regex, just structural tree edits.

Convergence — Run up to 5 passes. Refactoring can introduce new smells (e.g. the parameter object class created by Extract Parameter Object itself has primitive fields). Each pass re-detects and re-fixes until the code is clean or no further progress can be made.


2. Technology Stack

ToolVersionRole
Java17+Language (runs on Java 22)
JavaParser (symbol-solver-core)3.25.10AST parsing and in-place transformation
Maven3.9+Build system and dependency management
JUnit 5 (Jupiter)5.10.1Unit and integration testing
PIT Mutation Testing1.15.3Test quality validation
Java built-in HttpServerJDK built-inEmbedded web server (no extra dependency)

3. Project Structure

Project 1/
├── pom.xml ← Maven build file
├── README.md
├── .gitignore
└── src/
├── main/
│ ├── java/com/refactor/
│ │ ├── engine/
│ │ │ └── RefactoringEngine.java ← Main orchestrator + CLI entry point
│ │ ├── model/
│ │ │ ├── SmellType.java ← Enum of 8 smell categories
│ │ │ ├── CodeSmell.java ← A single detected violation
│ │ │ └── RefactorResult.java ← Outcome of one transformation
│ │ ├── report/
│ │ │ └── AnalysisReport.java ← Aggregated stats across all files
│ │ ├── smell/
│ │ │ ├── SmellDetector.java ← Interface: detect(CompilationUnit)
│ │ │ ├── LongMethodDetector.java
│ │ │ ├── LargeClassDetector.java
│ │ │ ├── LongParameterListDetector.java
│ │ │ ├── DuplicateCodeDetector.java
│ │ │ ├── GodClassDetector.java
│ │ │ └── PrimitiveObsessionDetector.java
│ │ └── transform/
│ │ ├── Refactorer.java ← Interface: refactor(cu, smell)
│ │ ├── LongMethodRefactorer.java ← Extract Method pattern
│ │ ├── LongParameterListRefactorer.java ← Introduce Parameter Object
│ │ └── GodClassRefactorer.java ← Extract Class / Delegate
│ └── resources/
│ └── web/
│ └── index.html ← Full frontend (HTML + CSS + JS)
└── test/
└── java/com/refactor/
├── LongMethodDetectorTest.java
├── LargeClassDetectorTest.java
├── LongParameterListDetectorTest.java
├── GodClassDetectorTest.java
├── GodClassDetectorExtendedTest.java
├── PrimitiveObsessionDetectorTest.java
├── DuplicateCodeDetectorTest.java
├── DuplicateCodeDetectorExtendedTest.java
├── DetectorThresholdsTest.java
├── LongMethodRefactorerTest.java
├── LongMethodRefactorerExtendedTest.java
├── LongParameterListRefactorerTest.java
├── GodClassRefactorerTest.java
├── GodClassRefactorerExtendedTest.java
├── SmellModelTest.java
├── AnalysisReportTest.java
├── AnalysisReportExtendedTest.java
├── RefactoringEngineTest.java
└── RefactoringEngineExtendedTest.java

4. Architecture

┌─────────────────────────────────────────────────────────────────────┐
│ Web Layer (port 8080) │
│ │
│ Browser ──POST /analyze──► AnalyzeHandler │
│ ▲ │ │
│ │ JSON response │ calls │
│ └──────────────────────────────┘ │
│ ▼ │
│ RefactoringEngine │
│ (analyseAndRefactorUntilClean) │
│ │ │
│ ┌──────────────────────┼──────────────────────┐ │
│ ▼ ▼ ▼ │
│ detectSmells() applyRefactorings() cu.toString() │
│ │ │ │
│ ┌─────────▼──────────┐ ┌────────▼────────────┐ │
│ │ smell package │ │ transform package │ │
│ │ │ │ │ │
│ │ SmellDetector[] │ │ Refactorer[] │ │
│ │ - LongMethod │ │ - LongMethod │ │
│ │ - LargeClass │ │ - LongParamList │ │
│ │ - LongParamList │ │ - GodClass │ │
│ │ - DuplicateCode │ │ │ │
│ │ - GodClass │ └─────────────────────┘ │
│ │ - PrimObsession │ │
│ └────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ model package │ │
│ │ CodeSmell │ │
│ │ RefactorResult │ │
│ │ SmellType (enum) │ │
│ └─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ report package │ │
│ │ AnalysisReport │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

Key Interfaces

SmellDetector — one method, all detectors implement it:

List<CodeSmell> detect(CompilationUnitcu);

Refactorer — two methods, all transformers implement them:

RefactorResultrefactor(CompilationUnitcu, CodeSmellsmell);
booleancanHandle(CodeSmellsmell);

The engine iterates through registered refactorers and calls canHandle() to find a match, then calls refactor(). Adding a new refactorer requires zero changes to existing code — this is the Open/Closed Principle in practice.


5. How It Works — Step by Step

Step 1 — Parse

Java source code (string)
│
▼
StaticJavaParser.parse(source)
│
▼
CompilationUnit ← root of the AST
├── TypeDeclaration (ClassOrInterfaceDeclaration)
│ ├── FieldDeclaration
│ ├── MethodDeclaration
│ │ ├── Parameter
│ │ └── BlockStmt
│ │ └── Statement[]
│ └── ...
└── ...

JavaParser converts the raw Java text into a fully navigable tree. Every node in the tree can be read, modified, added to, or removed — changes are reflected when you call cu.toString().

Step 2 — Detection

Each SmellDetector walks the AST using cu.findAll(NodeType.class) and checks structural properties. For example, LongMethodDetector calls cu.findAll(MethodDeclaration.class) and counts statements in each body.

Step 3 — Transformation

Each Refactorer receives the same CompilationUnit object and the CodeSmell that was found. It navigates to the offending node, restructures it (e.g. splits a method in two, introduces a new class), and returns a RefactorResult containing whether it succeeded and what the new source looks like.

Step 4 — Convergence Loop

Pass 1: detect smells → apply fixes → some smells remain (introduced by fixes)
Pass 2: detect remaining smells → apply fixes → fewer smells
Pass 3: detect remaining smells → none found → STOP

The engine runs up to 5 passes. It stops early if a pass produces zero smells or if no refactorer was able to fix anything.

Step 5 — Report

AnalysisReport accumulates every CodeSmell and RefactorResult across all passes and computes stats: total smells, refactored, skipped, and refactor rate (%).


6. Code Smell Detectors — Full Explanation

6.1 LongMethodDetector

File:smell/LongMethodDetector.javaDefault threshold: > 20 statements

cu.findAll(MethodDeclaration.class).forEach(method -> {
intstatementCount = method.getBody()
.map(body -> body.getStatements().size())
.orElse(0);
if (statementCount > threshold) { /* report smell */ }
});

Visits every method in the file. Counts the direct child statements in the method body. Abstract methods (no body) are automatically skipped via orElse(0). The threshold is configurable — default is 20.


6.2 LargeClassDetector

File:smell/LargeClassDetector.javaDefault thresholds: > 10 fields OR > 20 methods

cu.findAll(ClassOrInterfaceDeclaration.class).forEach(cls -> {
if (cls.isInterface()) return; // skip interfacesintfields = cls.getFields().size();
intmethods = cls.getMethods().size();
if (fields > fieldThreshold) { /* report smell */ }
if (methods > methodThreshold) { /* report smell */ }
});

Checks field count and method count independently. A class can trigger two smells — one for fields and one for methods. Interfaces are excluded because they are contracts, not implementations.


6.3 LongParameterListDetector

File:smell/LongParameterListDetector.javaDefault threshold: > 4 parameters

cu.findAll(MethodDeclaration.class).forEach(method -> {
intparamCount = method.getParameters().size();
if (paramCount > threshold) { /* report smell */ }
});

A method with too many parameters is hard to call, hard to remember, and usually means the method is doing too many things. The fix is to group related parameters into a dedicated object.


6.4 DuplicateCodeDetector

File:smell/DuplicateCodeDetector.javaDefault minimum window: 3 consecutive statements

Uses sliding window fingerprinting:

Method m1 statements: [A, B, C, D, E]
Method m2 statements: [A, B, C, X, Y]
Window size = 3:
m1 windows: [A,B,C], [B,C,D], [C,D,E]
m2 windows: [A,B,C], [B,C,X], [C,X,Y]
[A,B,C] appears in both m1 AND m2 → DUPLICATE

Each statement is normalized to its AST class name (e.g. ExpressionStmt, IfStmt, ReturnStmt) so the fingerprint captures structure rather than literal values. A window is only flagged if it appears in at least 2 distinct methods — same-method repetition is ignored.


6.5 GodClassDetector

File:smell/GodClassDetector.javaDefault thresholds: ≥ 7 fields AND ≥ 15 methods AND ≥ 80 total statements

All three conditions must be true simultaneously:

intfields = cls.getFields().size();
intmethods = cls.getMethods().size();
intloc = cls.getMethods().stream()
.mapToInt(m -> m.getBody().map(b -> b.getStatements().size()).orElse(0))
.sum();
if (fields >= fieldThreshold && methods >= methodThreshold && loc >= locThreshold) {
/* report god class */
}

Requiring all three conditions avoids false positives. A data-transfer object might have many fields but few methods — that's fine. A utility class might have many methods but few fields — also fine. Only a class that is large in all three dimensions is truly a God Class.


6.6 PrimitiveObsessionDetector

File:smell/PrimitiveObsessionDetector.javaDefault threshold: > 5 primitive or String fields

longprimitiveCount = cls.getFields().stream()
.filter(this::isPrimitiveOrWrapper)
.count();
if (primitiveCount > threshold) { /* report smell */ }

A field is considered "primitive" if its type is a Java primitive (int, boolean, double, etc.) or a common wrapper/String type (String, Integer, Long, etc.). Classes with too many such fields are missing domain concepts — String firstName, String lastName, String email should probably be an Address or Person object.


7. Refactoring Transformations — Full Explanation

7.1 LongMethodRefactorer — Extract Method

File:transform/LongMethodRefactorer.javaAddresses:LONG_METHOD

Algorithm:

  1. Find the target MethodDeclaration in the AST by class name and method name.
  2. Get the list of statements from the body.
  3. Split at the midpoint: firstHalf (statements 0..n/2) and secondHalf (statements n/2..n).
  4. Create a new private void helper method named originalNameHelper containing secondHalf.
  5. Replace the original body with firstHalf + a single call statement originalNameHelper();.
  6. Add the helper method to the class.

Guard: Returns failure if the method has fewer than 4 statements (not worth extracting).

Before:

voidprocessOrder() {
// 22 statements
}

After:

voidprocessOrder() {
// first 11 statementsprocessOrderHelper();
}
privatevoidprocessOrderHelper() {
// last 11 statements
}

7.2 LongParameterListRefactorer — Introduce Parameter Object

File:transform/LongParameterListRefactorer.javaAddresses:LONG_PARAMETER_LIST

Algorithm:

  1. Collect all Parameter nodes from the target method.
  2. Build a new public static inner class named {MethodName}Params with one public field per original parameter and an all-args constructor.
  3. Add this class to the enclosing class.
  4. Clear the method's parameter list and replace it with a single {MethodName}Params params parameter.

Guard: Returns failure if the method has 1 or fewer parameters.

Before:

voidcreate(Stringname, intage, Stringemail, booleanactive) { ... }

After:

publicstaticclassCreateParams {
publicStringname;
publicintage;
publicStringemail;
publicbooleanactive;
publicCreateParams(Stringname, intage, Stringemail, booleanactive) {
this.name = name;
this.age = age;
this.email = email;
this.active = active;
}
}
voidcreate(CreateParamsparams) { ... }

7.3 GodClassRefactorer — Extract Class

File:transform/GodClassRefactorer.javaAddresses:GOD_CLASS

Algorithm:

  1. Split the class's fields at the midpoint. Split its methods at the midpoint.
  2. Build a new public static inner class named {ClassName}Delegate containing the second halves.
  3. Remove the moved fields and methods from the original class.
  4. Add a private {ClassName}Delegate delegate = new {ClassName}Delegate(); field to the original class.

Guard: Returns failure if the class doesn't have at least 2 fields or 2 methods to split.

Before:

classOrderService { // 10 fields, 20 methods
...
}

After:

classOrderService { // 5 fields, 10 methods + delegate fieldprivateOrderServiceDelegatedelegate = newOrderServiceDelegate();
publicstaticclassOrderServiceDelegate { // 5 fields, 10 methods
...
}
}

8. Data Model

SmellType (enum)

Eight smell categories, each with a display name and description:

ConstantDisplay Name
LONG_METHODLong Method
LARGE_CLASSLarge Class
DUPLICATE_CODEDuplicate Code
LONG_PARAMETER_LISTLong Parameter List
GOD_CLASSGod Class
FEATURE_ENVYFeature Envy
DATA_CLUMPSData Clumps
PRIMITIVE_OBSESSIONPrimitive Obsession

CodeSmell

Immutable record of one detected violation:

FieldTypeDescription
typeSmellTypeWhich smell was found
classNameStringThe class it was found in
memberNameStringMethod or field name (null for class-level smells)
lineintLine number in the source file
detailStringHuman-readable explanation

RefactorResult

Outcome of one transformation attempt:

FieldTypeDescription
classNameStringClass that was transformed
smellAddressedSmellTypeWhich smell was being fixed
successbooleanWhether the transformation succeeded
refactoredCodeStringFull source after transformation (null if failed)
messageStringSummary of what happened

9. Analysis Report

AnalysisReport accumulates results across all passes and exposes:

MethodDescription
totalSmells()All smells detected across all passes
totalRefactored()Successful transformations
totalSkipped()Failed or unhandled smells
refactorRate()totalRefactored / totalSmells * 100
smellsByType()Map<SmellType, Long> — count per category
getDetectedSmells()Unmodifiable list of all CodeSmell objects
getRefactorResults()Unmodifiable list of all RefactorResult objects
summary()Full formatted text report

10. Web Server & Frontend

WebServer.java

Uses Java's built-in com.sun.net.httpserver.HttpServerno extra dependencies. Starts on port 8080 with a thread pool of 4.

Routes:

  • GET / → serves index.html from the classpath
  • POST /analyzeAnalyzeHandler

AnalyzeHandler.java

Handles the analysis API:

  1. Reads the request body (JSON with {"code": "..."})
  2. Parses the Java source with StaticJavaParser
  3. Calls engine.analyseAndRefactorUntilClean(cu, 5) (multi-pass)
  4. Detects remaining smells on the final cu for the "Remaining" stat
  5. Returns a JSON response with smells, results, stats, and the full refactored source

index.html (Frontend)

Single-file frontend — pure HTML, CSS, and vanilla JavaScript. No frameworks, no build step.

Left panel: Code editor (<textarea>) with monospace font and 3 sample snippet buttons.

Right panel — two tabs:

  • Analysis Results — stat cards (Detected / Refactored / Skipped / Rate / Remaining), smell cards with type + location + detail, refactoring result cards with ✅/⏭️ status
  • Refactored Code — full refactored Java source with minimal syntax highlighting (keywords, types, strings, comments, numbers) and a Copy button

Keyboard shortcut:Cmd+Enter / Ctrl+Enter triggers analysis.


11. How to Run

Prerequisites

  • Java 17 or higher: java -version
  • Maven 3.9+: mvn -version
    • If not installed on macOS: brew install maven

Run the Web UI

cd"Project 1"
mvn compile
mvn exec:java

Then open http://localhost:8080 in your browser.

Press Ctrl+C in the terminal to stop the server.

Run Tests

mvn clean test

Run a Specific Test Class

mvn test -Dtest=LongMethodDetectorTest

Run Mutation Testing

mvn org.pitest:pitest-maven:mutationCoverage

HTML report: target/pit-reports/index.html

Run the CLI (scan a directory)

mvn exec:java -Dexec.mainClass="com.refactor.engine.RefactoringEngine" \
-Dexec.args="src/main/java"

Use the Engine Programmatically

importcom.refactor.engine.RefactoringEngine;
importcom.github.javaparser.StaticJavaParser;
importcom.github.javaparser.ast.CompilationUnit;
StringjavaSource = "public class Foo { ... }";
CompilationUnitcu = StaticJavaParser.parse(javaSource);
RefactoringEngineengine = newRefactoringEngine();
AnalysisReportreport = engine.analyseAndRefactorUntilClean(cu, 5);
System.out.println(report.summary());
System.out.println(cu.toString()); // refactored source

Customize Thresholds

newRefactoringEngine(
List.of(
newLongMethodDetector(15), // flag methods > 15 statementsnewLargeClassDetector(8, 15), // flag > 8 fields or > 15 methodsnewLongParameterListDetector(3), // flag > 3 parametersnewDuplicateCodeDetector(4), // require 4+ matching statementsnewGodClassDetector(5, 12, 60), // tighter god-class thresholdsnewPrimitiveObsessionDetector(4) // flag > 4 primitive fields
),
List.of(
newLongMethodRefactorer(),
newLongParameterListRefactorer(),
newGodClassRefactorer()
)
);

12. Test Suite

105 JUnit 5 tests across 19 test classes.

Test ClassTestsWhat It Covers
LongMethodDetectorTest6Detection, threshold boundary, abstract method skipping, multiple methods
LargeClassDetectorTest3Field threshold, method threshold, interface exclusion
LongParameterListDetectorTest3Detection, correct name reporting, threshold
GodClassDetectorTest3Triple-condition logic, normal class, partial-condition non-detection
GodClassDetectorExtendedTest6Interface exclusion, boundary values, total LOC counting
PrimitiveObsessionDetectorTest2Primitive detection, domain-object exclusion
DuplicateCodeDetectorTest4Cross-method detection, empty class, default threshold
DuplicateCodeDetectorExtendedTest7Sliding window boundaries, single-method, below-min edge case
DetectorThresholdsTest18Off-by-one boundary tests for every detector
LongMethodRefactorerTest3Success, missing class/method, canHandle
LongMethodRefactorerExtendedTest5Helper added, body shortened, short-method guard, abstract method
LongParameterListRefactorerTest7Parameter object creation, naming, missing class/method, single-param guard
GodClassRefactorerTest6Delegate creation, missing class, not-enough-members guard
GodClassRefactorerExtendedTest4Target isolation, field/method removal, midpoint split accuracy
SmellModelTest7All getters, toString formats, SmellType enum completeness
AnalysisReportTest3Basic rate calculation, zero-smell case, summary labels
AnalysisReportExtendedTest10All count methods, map grouping, unmodifiable lists
RefactoringEngineTest3Smell detection, non-negative rate, smellsByType map
RefactoringEngineExtendedTest5DI constructor, no-matching-refactorer, multi-smell detection

13. Mutation Testing with PIT

PIT automatically mutates the source code — flipping > to >=, removing method calls, replacing return values with null or 0 — then checks if the test suite catches each mutation. A mutation that goes undetected reveals a gap in test coverage.

Results

MetricValue
Total mutants generated184
Mutants killed by tests~128
Mutation score (killed / generated)~70%
Test strength (killed / covered)79%
Target75% ✅

Test strength is the primary metric — it only counts mutants on lines that tests actually execute. Uncovered lines are excluded from the denominator.

Mutation Operators Applied

OperatorExample
Conditionals Boundary>>=
Negate Conditionals== true== false
Remove Conditionalsif (x > 0)if (true)
Math+-
Void Method Callsremoves list.add(item)
Return Valuesreturn countreturn 0
Incrementsi++i--

14. Design Principles

SOLID Applied

PrincipleWhere Applied
Single ResponsibilityEach detector handles exactly one smell. Each refactorer handles exactly one transformation pattern. AnalysisReport only aggregates.
Open/ClosedNew detectors/refactorers can be added by implementing the interface. RefactoringEngine never needs to change.
Liskov SubstitutionAny SmellDetector or Refactorer can be swapped in the engine's list without changing behaviour.
Interface SegregationSmellDetector has one method. Refactorer has two minimal methods. No fat interfaces.
Dependency InversionRefactoringEngine depends on SmellDetector and Refactorer interfaces, not concrete classes. DI constructor supports testing with mocks/stubs.

Why JavaParser?

JavaParser produces a fully mutable AST — you can add, remove, and replace nodes and then call cu.toString() to get back valid Java source. Unlike regex or string manipulation, it understands the actual structure of Java code, making transformations semantically safe.

Why No External Web Framework?

Java's built-in com.sun.net.httpserver.HttpServer handles everything needed for this project. Adding Spring Boot or Spark Java would introduce hundreds of transitive dependencies for what is essentially two routes. Keeping it dependency-free makes the project easy to build and understand.

Conservative Transformations

The refactorers deliberately avoid over-automation:

  • Extract Method splits at the midpoint rather than analysing data flow — it is safe but not optimal.
  • Introduce Parameter Object leaves the method body's variable references unchanged — the developer completes the wiring.
  • Extract Class creates a static inner class rather than a separate file, avoiding filesystem side effects.

This ensures transformations never break compilation.


15. Limitations & Future Work

LimitationPotential Fix
Duplicate Code uses only structural node types — may produce false positivesInclude normalized variable names in the fingerprint
Extract Method does not perform data-flow analysis — the split point is always the midpointImplement live-variable analysis to find the optimal extraction boundary
Introduce Parameter Object does not rewrite method body referencesUse JavaParser's NameExpr visitor to rename paramparams.param
No multi-file symbol resolution — Feature Envy and Data Clumps are defined but not yet detectedEnable JavaParser's JavaSymbolSolver with a full classpath
Transformations mutate the AST in memory but do not write back to diskAdd Files.writeString(path, cu.toString()) in the engine loop
No CI/CD pipelineAdd GitHub Actions to run mvn test and PIT on every push

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages