A collection of experimental Eclipse JDT (Java Development Tools) cleanup plugins and tools. This repository demonstrates how to build custom JDT cleanups, quick fixes, and related tooling for Eclipse-based Java development.
Main Technologies: Eclipse JDT, Java 21, Maven/Tycho 5.0.1
Status: Work in Progress – All plugins are experimental and intended for testing purposes.
This project provides:
- Custom JDT Cleanup Plugins: Automated code transformations for encoding, JUnit migration, functional programming patterns, and more
- Eclipse Product Build: A complete Eclipse product with bundled features
- P2 Update Site: Installable plugins via Eclipse update mechanism
- Test Infrastructure: JUnit 5-based tests for all cleanup implementations
All plugins are work-in-progress and intended for experimentation and learning.
- Overview
- Build Instructions
- Eclipse Version Configuration
- Quickstart
- CI Status
- What's Included
- Projects
- sandbox_cleanup_application
- sandbox_encoding_quickfix
- Encoding Cleanup – Replace Platform Encoding with Explicit Charset
- Based on Test Coverage
- Cleanup Strategies
- Java Version Awareness
- Supported Classes and APIs
- Examples
- Aggregation Mode Example
- Additional Fixes
- Cleanup Mode × Java Version Matrix
- Usage
- Encoding Cleanup – Strategy Variants
- Charset Literal Replacement Table
- Limitations
- sandbox_extra_search
- sandbox_usage_view
- sandbox_platform_helper
- sandbox_tools
- sandbox_jface_cleanup
- sandbox_functional_converter
- Functional Converter Cleanup – Transform Imperative Loops into Functional Java 8 Streams
- Source and Test Basis
- Supported Transformations
- Examples
- Reductions (Accumulators)
- Not Yet Supported (Disabled Tests)
- Ignored Cases – No Cleanup Triggered
- Java Version Compatibility
- Cleanup Name & Activation
- Limitations
- Summary
- sandbox_junit
- sandbox_method_reuse
- sandbox_xml_cleanup
- Installation
- Contributing
- Release Process
- License
IMPORTANT: This project (main branch, targeting Eclipse 2025-09) requires Java 21 or later.
The project uses Tycho 5.0.1 which requires Java 21. Building with Java 17 or earlier will fail with:
UnsupportedClassVersionError: ... has been compiled by a more recent version of the Java Runtime (class file version 65.0)
Verify your Java version:
java -version # Should show Java 21 or laterTo build the project, including a WAR file that contains the update site, run:
mvn -Dinclude=web -Pjacoco verify- The product will be located in
sandbox_product/target - The WAR file will be located in
sandbox_web/target
This error occurs when building with Java 17 or earlier:
TypeNotPresentException: Type P2ArtifactRepositoryLayout not present
...class file version 65.0, this version only recognizes class file versions up to 61.0
Solution: Upgrade to Java 21 or later. Verify with java -version.
This usually indicates a Java version mismatch. Check that:
JAVA_HOMEis set to Java 21+java -versionshows Java 21+- Maven is using the correct Java version:
mvn -version
The Eclipse version (SimRel release) used by this project is not centrally configured. When updating to a new Eclipse release, you must update the version reference in multiple files throughout the repository.
When migrating to a new Eclipse version, update the following files:
-
pom.xml(root)- Repository URLs in the
<repositories>section - Example:
https://download.eclipse.org/releases/2025-09/ - Also update Orbit repository URL:
https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2025-09/
- Repository URLs in the
-
sandbox_target/eclipse.target- Primary Eclipse release repository URL in first
<location>block - Example:
<repository location="https://download.eclipse.org/releases/2025-09/"/> - Also update Orbit repository URL
- Primary Eclipse release repository URL in first
-
sandbox_product/category.xml- Repository reference location
- Example:
<repository-reference location="https://download.eclipse.org/releases/2025-09/" .../>
-
sandbox_product/sandbox.product- Repository locations in
<repositories>section - Example:
<repository location="https://download.eclipse.org/releases/2025-09/" .../>
- Repository locations in
-
sandbox_oomph/sandbox.setup- P2 repository URL in the version-specific
<setupTask>block - Example:
<repository url="https://download.eclipse.org/releases/2025-09"/>
- P2 repository URL in the version-specific
- Use HTTPS: All Eclipse download URLs should use
https://(nothttp://) - Use explicit versions: Prefer explicit version URLs (e.g.,
2025-09) overlatestfor reproducible builds - Keep versions aligned: All files should reference the same Eclipse SimRel version
- Git URLs: Use HTTPS for git clone URLs (e.g.,
https://github.com/..., notgit://) - Main branch: All Oomph setup files should reference the
mainbranch, notmaster
- Eclipse Version: 2025-09
- Java Version: 21
- Tycho Version: 5.0.1
- Default Branch:
main
After building the project, you can run the Eclipse product with the bundled cleanup plugins:
# Navigate to the product directory
cd sandbox_product/target/products/org.sandbox.product/
# Launch Eclipse
./eclipseYou can apply cleanup transformations using the Eclipse JDT formatter application pattern:
eclipse -nosplash -consolelog -debug \
-application org.eclipse.jdt.core.JavaCodeFormatter \
-verbose -config MyCleanupSettings.ini MyClassToCleanup.javaNote: Replace
MyCleanupSettings.iniwith your cleanup configuration file andMyClassToCleanup.javawith the Java file you want to process.
You can install the cleanup plugins into your existing Eclipse installation using the P2 update site:
- In Eclipse, go to Help → Install New Software...
- Click Add... and enter the update site URL (see Installation section)
- Select the desired cleanup features
- Follow the installation wizard
Warning: Use only with a test Eclipse installation. These plugins are experimental and may affect your IDE stability.
| Branch | Java Version | Tycho Version |
|---|---|---|
main (2025-09) |
Java 21 | 5.0.1 |
2024-06+ |
Java 21 | 5.0.x |
2022-12+ |
Java 17 | 4.x |
Up to 2022-06 |
Java 11 | 3.x |
Note: Tycho 5.x requires Java 21+ at build time. Attempting to build with Java 17 will result in UnsupportedClassVersionError.
- Building for different Eclipse versions via GitHub Actions
- Creating custom JDT cleanups
- Setting up the SpotBugs Maven plugin to fail the build on issues
- Writing JUnit 5-based tests for JDT cleanups
- Configuring JaCoCo for test coverage
- Building an Eclipse product including new features
- Automatically building a WAR file including a P2 update site
All projects are considered work in progress unless otherwise noted.
Placeholder for a CLI-based cleanup application, similar to the Java code formatting tool:
eclipse -nosplash -consolelog -debug -application org.eclipse.jdt.core.JavaCodeFormatter -verbose -config MyCodingStandards.ini MyClassToBeFormatted.javaSee: https://bugs.eclipse.org/bugs/show_bug.cgi?id=75333
The Encoding Cleanup replaces platform-dependent or implicit encoding usage with explicit, safe alternatives using StandardCharsets.UTF_8 or equivalent constants.
It supports multiple strategies and is Java-version-aware.
The cleanup logic is tested and verified by the following test files:
Java22/ExplicitEncodingPatterns.javaJava10/ExplicitEncodingPatternsPreferUTF8.javaJava10/ExplicitEncodingPatternsKeepBehavior.javaJava10/ExplicitEncodingPatternsAggregateUTF8.javaJava10/ExplicitEncodingCleanUpTest.java
| Strategy | Description |
|---|---|
| Prefer UTF-8 | Replace "UTF-8" and platform-default encodings with StandardCharsets.UTF_8 |
| Keep Behavior | Only fix cases where behavior is guaranteed not to change (e.g. "UTF-8" literal) |
| Aggregate UTF-8 | Replace all "UTF-8" usage with a shared static final Charset UTF_8 field |
| Java Version | Supported Transformations |
|---|---|
| Java < 7 | Cleanup is disabled – StandardCharsets not available |
| Java 7–10 | Basic replacements using StandardCharsets.UTF_8, stream wrapping, and exception removal |
| Java 11+ | Adds support for Files.newBufferedReader(path, charset) and Channels.newReader(...) |
| Java 21+ | Enables usage of Files.readString(...) and Files.writeString(...) with charset |
The cleanup covers a wide range of encoding-sensitive classes:
| Class / API | Encoding Behavior | Cleanup Action |
|---|---|---|
OutputStreamWriter |
Requires explicit encoding | Replace "UTF-8" or add missing StandardCharsets.UTF_8 |
InputStreamReader |
Same | Add StandardCharsets.UTF_8 where missing |
FileReader / FileWriter |
Implicit platform encoding | Replace with stream + InputStreamReader + charset |
Scanner(InputStream) |
Platform encoding | Add charset constructor if available (Java 10+) |
PrintWriter(OutputStream) |
Platform encoding | Use new constructor with charset if possible |
Files.newBufferedReader(Path) |
Platform encoding by default | Use overload with charset |
Files.newBufferedWriter(Path) |
Same | Use overload with charset |
Files.readAllLines(Path) |
Platform encoding | Use readAllLines(path, charset) if available |
Files.readString(Path) |
Available since Java 11 / 21+ | Use with charset overload |
Charset.forName("UTF-8") |
Literal resolution | Replace with StandardCharsets.UTF_8 |
Channels.newReader(...) |
Charset overload available since Java 11 | Use it when applicable |
InputSource.setEncoding(String) |
Not a stream – SAX API | Replace string literal "UTF-8" with constant if possible |
Before:
Reader r = new FileReader(file);After:
Reader r = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);Before:
Reader r = Channels.newReader(channel, "UTF-8");After:
Reader r = Channels.newReader(channel, StandardCharsets.UTF_8);Before:
List<String> lines = Files.readAllLines(path);After:
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);Before:
Scanner scanner = new Scanner(inputStream);After:
Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8);Before:
InputSource source = new InputSource();
source.setEncoding("UTF-8");After:
source.setEncoding(StandardCharsets.UTF_8.name());If Aggregate UTF-8 mode is enabled:
private static final Charset UTF_8 = StandardCharsets.UTF_8;
Reader r = new InputStreamReader(in, UTF_8);All uses of StandardCharsets.UTF_8 or "UTF-8" will be redirected to UTF_8.
- Adds required imports:
import java.nio.charset.StandardCharsets;import java.nio.charset.Charset;(if aggregation is used)
- Removes:
throws UnsupportedEncodingExceptionif replaced by standard charset"UTF-8"string constants if inlined
| Java Version | Prefer UTF-8 | Keep Behavior | Aggregate UTF-8 | Files.readString / Channels |
|---|---|---|---|---|
| Java 7 | ✅ | ✅ | ✅ | ❌ |
| Java 10 | ✅ | ✅ | ✅ | ❌ |
| Java 11–20 | ✅ | ✅ | ✅ | ✅ |
| Java 21+ | ✅ | ✅ | Optional | ✅ (modern API encouraged) |
- Via Eclipse Clean Up... under Encoding category
- Via JDT Batch tooling, with properties:
encoding.strategy = PREFER_UTF8 | KEEP | AGGREGATEaggregate.charset.name = UTF_8min.java.version = 7 | 10 | 11 | 21
The Encoding Cleanup supports multiple strategies depending on the selected configuration. Each strategy affects which code constructs are transformed and how safely defaults are preserved.
Replaces all literal "UTF-8" occurrences and platform-default encodings with StandardCharsets.UTF_8.
Before:
new InputStreamReader(in);
new FileReader(file);
Charset.forName("UTF-8");After:
new InputStreamReader(in, StandardCharsets.UTF_8);
new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
StandardCharsets.UTF_8;Only transforms code if "UTF-8" is explicitly used – avoids changing platform-default behaviors.
Before:
Charset charset = Charset.forName("UTF-8");
new InputStreamReader(in);After:
Charset charset = StandardCharsets.UTF_8;
new InputStreamReader(in); // left unchangedReplaces all "UTF-8" usage and StandardCharsets.UTF_8 with a class-level constant.
Before:
new InputStreamReader(in, StandardCharsets.UTF_8);
new FileReader(file);After:
private static final Charset UTF_8 = StandardCharsets.UTF_8;
new InputStreamReader(in, UTF_8);
new InputStreamReader(new FileInputStream(file), UTF_8);Also supports dynamic replacement of:
Charset.forName("UTF-8")"UTF-8"literals passed to methods likesetEncoding(...)
| Strategy | Platform Default Handling | Replaces "UTF-8" |
Aggregates Constant |
|---|---|---|---|
| Prefer UTF-8 | Yes | Yes | No |
| Keep Behavior | No | Yes (only explicit) | No |
| Aggregate UTF-8 | Yes | Yes | Yes (UTF_8) |
These strategies are controlled via cleanup preferences:
encoding.strategy = PREFER_UTF8 | KEEP | AGGREGATE
The cleanup recognizes common charset string literals and replaces them with the appropriate constants from StandardCharsets:
| String Literal | Replacement Constant |
|---|---|
"UTF-8" |
StandardCharsets.UTF_8 |
"US-ASCII" |
StandardCharsets.US_ASCII |
"ISO-8859-1" |
StandardCharsets.ISO_8859_1 |
"UTF-16" |
StandardCharsets.UTF_16 |
"UTF-16BE" |
StandardCharsets.UTF_16BE |
"UTF-16LE" |
StandardCharsets.UTF_16LE |
- Dynamic encodings (read from config or variables) are left untouched
- Aggregation introduces class-level fields (may require conflict checks)
- Cleanup logic avoids modifying non-I/O encoding usages
This documentation is based on test-driven implementations in the sandbox_encoding_quickfix_test module and reflects support for modern and legacy encoding cleanup across Java 7 to 22.
Reference: JEP 400: UTF-8 by Default – Partial implementation to highlight platform encoding usage via API changes.
Experimental search tool for identifying critical classes when upgrading Eclipse or Java versions.
Provides a table view of code objects, sorted by name, to detect inconsistent naming that could confuse developers.
This cleanup modernizes the usage of org.eclipse.core.runtime.Status in Eclipse-based projects by replacing verbose constructor calls with cleaner alternatives.
It supports two strategies, depending on the Java and Eclipse platform version:
- Java 8 / Eclipse < 4.20: Use a project-specific
StatusHelperclass. - Java 11+ / Eclipse ≥ 4.20: Use the static factory methods
Status.error(...),Status.warning(...),Status.info(...), etc.
The cleanup logic is based on:
Constructing IStatus instances via new Status(...) is verbose and error-prone. This cleanup provides more readable alternatives by:
- Reducing boilerplate code
- Unifying the way status objects are created
- Encouraging use of centralized helpers or platform-provided factories
| Case Type | Legacy Code | Cleanup Result (Java 8) | Cleanup Result (Java 11 / Eclipse ≥ 4.20) |
|---|---|---|---|
| Basic warning | new Status(IStatus.WARNING, id, msg) |
(unchanged – concise) | (unchanged – concise) |
| With 4 arguments | new Status(IStatus.WARNING, id, msg, null) |
StatusHelper.warning(id, msg) |
Status.warning(msg) |
| With exception | new Status(IStatus.ERROR, id, msg, e) |
StatusHelper.error(id, msg, e) |
Status.error(msg, e) |
| INFO with 4 args | new Status(IStatus.INFO, id, code, msg, null) |
StatusHelper.info(id, msg) |
Status.info(msg) |
| OK status | new Status(IStatus.OK, id, "done") |
(unchanged – already minimal) | (unchanged – already minimal) |
Before:
IStatus status = new Status(IStatus.WARNING, "plugin.id", "Something happened", null);After:
IStatus status = StatusHelper.warning("plugin.id", "Something happened");Before:
IStatus status = new Status(IStatus.WARNING, "plugin.id", IStatus.OK, "Something happened", null);After:
IStatus status = Status.warning("Something happened");Before:
IStatus status = new Status(IStatus.ERROR, "plugin.id", "Something bad happened", exception);After (Java 8):
IStatus status = StatusHelper.error("plugin.id", "Something bad happened", exception);After (Java 11+):
IStatus status = Status.error("Something bad happened", exception);| Target Platform | Strategy Used |
|---|---|
| Eclipse < 4.20 | Insert StatusHelper method calls |
| Eclipse 4.20 or newer | Replace with Status.error(...), Status.warning(...), etc. |
- For Status factory methods: Eclipse Platform 4.20+ and Java 11+
- For StatusHelper: Either implement your own helper, or use a generated version
- Static import of
org.eclipse.core.runtime.Statusis recommended
This cleanup is available as part of the JDT Clean Up framework. It can be run via:
- Eclipse UI → Source → Clean Up
- Automated build tools using Eclipse JDT APIs or Maven plugins
- Only applies to direct calls to the
Statusconstructor - Plugin ID handling is simplified – if it must be retained dynamically, manual changes may be needed
- Custom
IStatussubclasses or complex logic are not handled
This documentation is based on the cleanup logic and test cases in Java8CleanUpTest.java and Java9CleanUpTest.java. Manual review is advised for edge cases or plugin-specific conventions.
Reference: Eclipse 4.20 Platform ISV – Simpler Status Creation – PoC for a QuickFix to migrate code based on new platform features.
While-to-For loop converter — already merged into Eclipse JDT.
The JFace Cleanup automates the migration from the deprecated SubProgressMonitor API to the modern SubMonitor API introduced in Eclipse 3.4.
SubProgressMonitor has been deprecated in favor of SubMonitor, which provides:
- Simpler API: Fluent interface with method chaining
- Better null safety: Built-in null handling for progress monitors
- Improved work allocation: More intuitive split() semantics
- Idempotent transformations: The cleanup can be run multiple times safely
- Forward compatibility: SubMonitor is the recommended API since Eclipse 3.4+
This cleanup is designed to be idempotent – already migrated code will not be transformed again, ensuring safe repeated application.
The cleanup transforms the classic beginTask() + SubProgressMonitor pattern into the modern SubMonitor.convert() + split() pattern.
Before:
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
public void doWork(IProgressMonitor monitor) {
monitor.beginTask("Main Task", 100);
IProgressMonitor sub1 = new SubProgressMonitor(monitor, 60);
IProgressMonitor sub2 = new SubProgressMonitor(monitor, 40);
}After:
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
public void doWork(IProgressMonitor monitor) {
SubMonitor subMonitor = SubMonitor.convert(monitor, "Main Task", 100);
IProgressMonitor sub1 = subMonitor.split(60);
IProgressMonitor sub2 = subMonitor.split(40);
}The cleanup also handles SubProgressMonitor constructor calls with style flags:
Before:
IProgressMonitor sub = new SubProgressMonitor(monitor, 50, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK);After:
SubMonitor subMonitor = SubMonitor.convert(monitor, "Task", 100);
IProgressMonitor sub = subMonitor.split(50, SubMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK);If the scope already contains a variable named subMonitor, the cleanup generates a unique name:
Before:
public void doWork(IProgressMonitor monitor) {
String subMonitor = "test"; // Name collision
monitor.beginTask("Task", 100);
IProgressMonitor sub = new SubProgressMonitor(monitor, 50);
}After:
public void doWork(IProgressMonitor monitor) {
String subMonitor = "test";
SubMonitor subMonitor2 = SubMonitor.convert(monitor, "Task", 100);
IProgressMonitor sub = subMonitor2.split(50);
}The cleanup is idempotent – code that has already been migrated to SubMonitor will not be modified:
Input (Already Migrated):
public void doWork(IProgressMonitor monitor) {
SubMonitor subMonitor = SubMonitor.convert(monitor, "Task", 100);
IProgressMonitor sub = subMonitor.split(50);
IProgressMonitor sub2 = subMonitor.split(30);
}Output:
// No changes - code is already using SubMonitor pattern
public void doWork(IProgressMonitor monitor) {
SubMonitor subMonitor = SubMonitor.convert(monitor, "Task", 100);
IProgressMonitor sub = subMonitor.split(50);
IProgressMonitor sub2 = subMonitor.split(30);
}- SubMonitor API: SubMonitor JavaDoc
- SubProgressMonitor (Deprecated): SubProgressMonitor JavaDoc
- Eclipse 3.4 Migration Guide: SubMonitor was introduced in Eclipse 3.4 as the preferred way to handle progress monitors
- Eclipse Version: 3.4+ (for
SubMonitorAPI availability) - Java Version: Compatible with Java 8+
| Eclipse Cleanup ID | Value |
|---|---|
MYCleanUpConstants.JFACE_CLEANUP |
true (enable this feature) |
Usage:
- Via Eclipse Clean Up... under the JFace category
- Via Save Actions in Eclipse preferences
- Via JDT Batch tooling
- Only transforms code that follows the standard pattern of
monitor.beginTask()followed bynew SubProgressMonitor() - Does not handle cases where monitors are passed through multiple layers without the beginTask call
- Name collision resolution uses simple numeric suffixes (subMonitor2, subMonitor3, etc.)
The cleanup is thoroughly tested in:
sandbox_jface_cleanup_test/src/org/sandbox/jdt/ui/tests/quickfix/Java8CleanUpTest.java
Test cases cover:
- Basic transformation patterns
- Multiple SubProgressMonitor instances per method
- Style flags (2-arg and 3-arg constructors)
- Variable name collisions
- Idempotence verification
- Mixed scenarios (some methods converted, others not)
- Nested classes and inner classes
- Import handling when SubProgressMonitor and SubMonitor imports coexist
This cleanup modernizes imperative Java loop constructs by transforming them into functional-style equivalents using Java 8 Streams, map, filter, reduce, and forEach.
📐 Architecture Documentation: See ARCHITECTURE.md for detailed implementation details, design patterns, and internal components.
This cleanup is fully tested in:
sandbox_functional_converter_test/src/org/sandbox/jdt/ui/tests/quickfix/Java8CleanUpTest.java
The test class defines:
- 25 enabled test cases covering fully supported loop transformation patterns
- A list of
@Disabledscenarios representing future features and unsupported patterns - A set of
@ValueSourcecases where no transformation should be applied (edge cases)
The cleanup currently supports the following patterns:
| Pattern | Transformed To |
|---|---|
| Simple enhanced for-loops | list.forEach(...) or list.stream().forEach(...) |
| Mapping inside loops | .stream().map(...) |
Filtering via if or continue |
.stream().filter(...) |
| Null safety checks | .filter(l -> l != null).map(...) |
| Reductions (sum/counter) | .stream().map(...).reduce(...) |
| MAX/MIN reductions | .reduce(init, Math::max) or .reduce(init, Math::min) |
String concatenation in loops |
.reduce(..., String::concat) |
Conditional early return true |
.anyMatch(...) |
Conditional early return false |
.noneMatch(...) |
| Conditional check all valid | .allMatch(...) |
| Method calls inside mapping/filtering | map(x -> method(x)), filter(...) |
Combined filter, map, forEach |
Chained stream transformations |
| Nested conditionals | Multiple .filter(...) operations |
| Increment/decrement reducers | .map(_item -> 1).reduce(0, Integer::sum) |
| Compound assignment reducers | .map(expr).reduce(init, operator) |
Enabled Test Cases (25 total):
SIMPLECONVERT,CHAININGMAP,ChainingFilterMapForEachConvertSmoothLongerChaining,MergingOperations,BeautificationWorks,BeautificationWorks2NonFilteringIfChaining,ContinuingIfFilterSingleStatementSimpleReducer,ChainedReducer,IncrementReducer,AccumulatingMapReduceDOUBLEINCREMENTREDUCER,DecrementingReducer,ChainedReducerWithMerging,StringConcatChainedAnyMatch,ChainedNoneMatchNoNeededVariablesMerging,SomeChainingWithNoNeededVarMaxReducer,MinReducer,MaxWithExpression,MinWithExpressionFilteredMaxReduction,ChainedMapWithMinReduction,ComplexFilterMapMaxReductionContinueWithMapAndForEachSimpleAllMatch,AllMatchWithNullCheck,ChainedAllMatchNestedFilterCombination
Before:
for (Integer l : list) {
System.out.println(l);
}After:
list.forEach(l -> System.out.println(l));Before:
for (Integer l : list) {
if (l != null) {
String s = l.toString();
System.out.println(s);
}
}After:
list.stream()
.filter(l -> (l != null))
.map(l -> l.toString())
.forEachOrdered(s -> {
System.out.println(s);
});Before:
for (Integer l : list) {
if (l == null) {
continue;
}
String s = l.toString();
System.out.println(s);
}After:
list.stream()
.filter(l -> !(l == null))
.map(l -> l.toString())
.forEachOrdered(s -> {
System.out.println(s);
});Before:
for (Integer l : list) {
String s = l.toString();
Object o = foo(s);
if (o == null)
return true;
}
return false;After:
if (list.stream()
.map(l -> l.toString())
.map(s -> foo(s))
.anyMatch(o -> (o == null))) {
return true;
}
return false;Before:
for (String item : items) {
if (!item.startsWith("valid")) {
return false;
}
}
return true;After:
if (!items.stream().allMatch(item -> item.startsWith("valid"))) {
return false;
}
return true;Before:
int max = Integer.MIN_VALUE;
for (Integer num : numbers) {
max = Math.max(max, num);
}After:
int max = Integer.MIN_VALUE;
max = numbers.stream().reduce(max, Math::max);Similarly for Math.min() → .reduce(min, Math::min)
Before:
int maxLen = 0;
for (String str : strings) {
maxLen = Math.max(maxLen, str.length());
}After:
int maxLen = 0;
maxLen = strings.stream()
.map(str -> str.length())
.reduce(maxLen, Math::max);Before:
for (String item : items) {
if (item != null) {
if (item.length() > 5) {
System.out.println(item);
}
}
}After:
items.stream()
.filter(item -> (item != null))
.filter(item -> (item.length() > 5))
.forEachOrdered(item -> {
System.out.println(item);
});Before:
int count = 0;
for (String s : list) {
count += 1;
}After:
int count = list.stream()
.map(_item -> 1)
.reduce(0, Integer::sum);Before:
int sum = 0;
for (Integer l : list) {
sum += foo(l);
}After:
int sum = list.stream()
.map(l -> foo(l))
.reduce(0, Integer::sum);Also supported:
- Decrementing:
i -= 1→.reduce(i, (a, b) -> a - b) - Type-aware literals:
1for int,1Lfor long,1.0for double,1.0ffor float - String concatenation:
.reduce("", String::concat)
The following patterns are currently not supported and are marked @Disabled in the test suite:
| Pattern Description | Reason / Required Feature |
|---|---|
Map.put(...) inside loop |
Needs Collectors.toMap(...) support |
Early break inside loop body |
Requires stream short-circuit modeling (findFirst()) |
Labeled continue or break (label:) |
Not expressible via Stream API |
Complex if-else-return branches |
Requires flow graph and branching preservation |
throw inside loop |
Non-convertible – not compatible with Stream flow |
| Multiple accumulators in one loop | State mutation not easily transferable |
These patterns are intentionally excluded from transformation to maintain semantic correctness and safety.
The cleanup does not modify code in the following edge cases (validated by @ValueSource tests):
- Non-loop constructs
- Loops over arrays instead of
ListorIterable - Loops with early
return,throw, or labeledcontinue - Loops mixing multiple mutable accumulators
- Loops with side effects that cannot be safely preserved
| API Used | Requires Java |
|---|---|
Stream, map, filter |
Java 8+ |
forEach, forEachOrdered |
Java 8+ |
anyMatch, noneMatch |
Java 8+ |
reduce |
Java 8+ |
Collectors.toList() |
Java 8+ |
This cleanup is designed for Java 8+ projects and uses only APIs available since Java 8.
| Eclipse Cleanup ID | Value |
|---|---|
MYCleanUpConstants.USEFUNCTIONALLOOP_CLEANUP |
true (enable this feature) |
Usage:
- Via Eclipse Clean Up... under the appropriate cleanup category
- Via JDT Batch tooling or Save Actions
- Does not preserve external loop-scoped variables (e.g., index tracking, multiple accumulators)
- Cannot convert control structures with
return,break,continue label, orthrow - Does not support loops producing
Map<K,V>outputs or grouping patterns (future feature) - Does not merge consecutive filters/maps (could be optimized in future versions)
The Functional Converter Cleanup:
- Applies safe and proven transformations across 21 tested patterns
- Targets common loop structures found in legacy codebases
- Modernizes Java 5/6/7-style loops to Java 8 stream-based idioms
- Uses an extensive test suite for coverage and correctness
- Maintains semantic safety by excluding complex patterns
Further Reading:
- Implementation Details: ARCHITECTURE.md – In-depth architecture documentation
- Test Coverage:
Java8CleanUpTest.javain thesandbox_functional_converter_testmodule - Wiki: Functional Converter – Converts
Iteratorloops to functional loops
The JUnit Cleanup tool automates the migration of legacy tests from JUnit 3 and JUnit 4 to JUnit 5.
It is based on verified transformations from the following test files:
JUnit3CleanupCases.javaJUnitCleanupCases.java
The cleanup handles a comprehensive set of JUnit 3 and JUnit 4 constructs, including:
- Class structure:
extends TestCase, test method naming conventions - Lifecycle methods: Setup, teardown, class-level initialization
- Annotations: Test markers, lifecycle annotations, ignore/disable markers
- Assertions: All JUnit assertion methods with parameter reordering
- Assumptions: Precondition checking methods
- Test suites: Suite runners and configuration
- Rules:
@Rule,@ClassRule, includingTemporaryFolder,TestName,ExternalResource
The cleanup tool handles all major JUnit 3 constructs used in legacy test codebases.
| JUnit 3 Construct | Description | JUnit 5 Equivalent |
|---|---|---|
junit.framework.TestCase |
Base class for tests | (removed) – no base class needed |
extends TestCase |
Class inheritance | (removed) – use annotations instead |
public void testXxx() |
Test method naming convention | @Test void xxx() – descriptive names |
protected void setUp() |
Setup before each test | @BeforeEach void setUp() |
protected void tearDown() |
Cleanup after each test | @AfterEach void tearDown() |
public static Test suite() |
Test suite definition | @TestMethodOrder + @Order annotations |
TestSuite.addTest(...) |
Adding tests to suite | Individual @Test methods with ordering |
junit.framework.Assert.* |
Assertion methods | org.junit.jupiter.api.Assertions.* |
The cleanup removes the extends TestCase inheritance and eliminates the need for JUnit 3's base class.
Before:
import junit.framework.TestCase;
public class MyTest extends TestCase {
public MyTest(String name) {
super(name);
}
}After:
import org.junit.jupiter.api.Test;
public class MyTest {
// Constructor removed - no longer needed
}Changes Applied:
- Remove
extends TestCasefrom class declaration - Remove constructor that calls
super(name) - Remove
import junit.framework.TestCase - Add appropriate JUnit 5 imports
JUnit 3 uses naming conventions (testXxx) to identify test methods. JUnit 5 uses the @Test annotation.
Before:
public void testBasicAssertions() {
assertEquals("Values should match", 42, 42);
assertTrue("Condition should be true", true);
}After:
@Test
@Order(1)
public void testBasicAssertions() {
assertEquals(42, 42, "Values should match");
assertTrue(true, "Condition should be true");
}Changes Applied:
- Add
@Testannotation - Add
@Orderannotation if part of a suite (maintains test execution order) - Reorder assertion parameters (message moves to last position)
- Optionally rename to more descriptive names (removing
testprefix)
JUnit 3 uses method name conventions for setup and teardown. JUnit 5 uses annotations.
Before:
@Override
protected void setUp() throws Exception {
// Setup before each test
}
@Override
protected void tearDown() throws Exception {
// Cleanup after each test
}After:
@BeforeEach
public void setUp() throws Exception {
// Setup before each test
}
@AfterEach
public void tearDown() throws Exception {
// Cleanup after each test
}Changes Applied:
- Replace implicit naming convention with
@BeforeEachannotation - Replace implicit naming convention with
@AfterEachannotation - Remove
@Overrideannotation (no longer overriding from base class) - Methods can remain
protectedor becomepublic
JUnit 3 uses suite() methods and TestSuite class. JUnit 5 uses @TestMethodOrder with ordering annotations.
Before:
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(new MyTest("testBasicAssertions"));
suite.addTest(new MyTest("testArrayAssertions"));
suite.addTest(new MyTest("testWithAssume"));
return suite;
}After:
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class MyTest {
@Test
@Order(1)
public void testBasicAssertions() { }
@Test
@Order(2)
public void testArrayAssertions() { }
@Test
@Order(3)
public void testWithAssume() { }
}Changes Applied:
- Remove
suite()method completely - Add
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)to class - Add
@Order(n)annotations to maintain execution order - Add required imports:
org.junit.jupiter.api.TestMethodOrderorg.junit.jupiter.api.MethodOrdererorg.junit.jupiter.api.Order
The cleanup tool handles all major JUnit 4 annotations, lifecycle methods, and special constructs.
| JUnit 4 Construct | Description | JUnit 5 Equivalent |
|---|---|---|
@Test |
Test method marker | @Test (from org.junit.jupiter.api) |
@Before |
Setup before each test | @BeforeEach |
@After |
Cleanup after each test | @AfterEach |
@BeforeClass |
Setup before all tests (static) | @BeforeAll |
@AfterClass |
Cleanup after all tests (static) | @AfterAll |
@Ignore |
Disable a test | @Disabled |
@Ignore("reason") |
Disable with message | @Disabled("reason") |
@Test(expected = Ex.class) |
Expected exception test | assertThrows(Ex.class, () -> {...}) |
@Test(timeout = ms) |
Timeout test | assertTimeout(Duration.ofMillis(ms), ...) |
@RunWith(Suite.class) |
Suite runner | @Suite |
@Suite.SuiteClasses({...}) |
Suite configuration | @SelectClasses({...}) |
@Rule |
Test rule (instance-level) | @RegisterExtension |
@ClassRule |
Test rule (class-level, static) | @RegisterExtension (static) |
@FixMethodOrder |
Test method ordering | @TestMethodOrder |
org.junit.Assert.* |
Assertion methods | org.junit.jupiter.api.Assertions.* |
org.junit.Assume.* |
Assumption methods | org.junit.jupiter.api.Assumptions.* |
JUnit 4 lifecycle annotations are replaced with JUnit 5 equivalents.
Before:
import org.junit.Before;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.AfterClass;
@BeforeClass
public static void setUpBeforeClass() throws Exception {
// Setup before all tests
}
@AfterClass
public static void tearDownAfterClass() throws Exception {
// Cleanup after all tests
}
@Before
public void setUp() throws Exception {
// Setup before each test
}
@After
public void tearDown() throws Exception {
// Cleanup after each test
}After:
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
@BeforeAll
public static void setUpBeforeClass() throws Exception {
// Setup before all tests
}
@AfterAll
public static void tearDownAfterClass() throws Exception {
// Cleanup after all tests
}
@BeforeEach
public void setUp() throws Exception {
// Setup before each test
}
@AfterEach
public void tearDown() throws Exception {
// Cleanup after each test
}Changes Applied:
@Before→@BeforeEach@After→@AfterEach@BeforeClass→@BeforeAll@AfterClass→@AfterAll- Update imports accordingly
@Test Annotation:
Basic @Test annotation is migrated from JUnit 4 to JUnit 5:
Before:
import org.junit.Test;
@Test
public void myTest() {
// test code
}After:
import org.junit.jupiter.api.Test;
@Test
public void myTest() {
// test code
}@Ignore / @Disabled:
Before:
import org.junit.Ignore;
import org.junit.Test;
@Ignore
@Test
public void ignoredTestWithoutMessage() {
fail("This test is ignored");
}
@Ignore("Ignored with message")
@Test
public void ignoredTestWithMessage() {
fail("This test is ignored with a message");
}After:
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@Disabled
@Test
public void ignoredTestWithoutMessage() {
Assertions.fail("This test is ignored");
}
@Disabled("Ignored with message")
@Test
public void ignoredTestWithMessage() {
Assertions.fail("This test is ignored with a message");
}Changes Applied:
@Ignore→@Disabled@Ignore("reason")→@Disabled("reason")- Update imports
Before:
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
MyTest.class,
OtherTest.class
})
public class MyTestSuite {
}After:
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SelectClasses;
@Suite
@SelectClasses({
MyTest.class,
OtherTest.class
})
public class MyTestSuite {
}Changes Applied:
@RunWith(Suite.class)→@Suite@Suite.SuiteClasses({...})→@SelectClasses({...})- Update imports to JUnit Platform Suite API
JUnit 4 @Rule and @ClassRule are migrated to JUnit 5 @RegisterExtension.
TemporaryFolder Rule:
Before:
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
@Test
public void test() throws IOException {
File newFile = tempFolder.newFile("myfile.txt");
}After:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
@TempDir
Path tempFolder;
@Test
public void test() throws IOException {
File newFile = tempFolder.resolve("myfile.txt").toFile();
}TestName Rule:
Before:
import org.junit.Rule;
import org.junit.rules.TestName;
@Rule
public TestName tn = new TestName();
@Test
public void test() {
System.out.println("Test name: " + tn.getMethodName());
}After:
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.BeforeEach;
private String testName;
@BeforeEach
void init(TestInfo testInfo) {
this.testName = testInfo.getDisplayName();
}
@Test
public void test() {
System.out.println("Test name: " + testName);
}ExternalResource Rule:
Before:
import org.junit.Rule;
import org.junit.rules.ExternalResource;
@Rule
public ExternalResource er = new ExternalResource() {
@Override
protected void before() throws Throwable {
// setup
}
@Override
protected void after() {
// cleanup
}
};After:
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
@RegisterExtension
public Er_5b8b4 er = new Er_5b8b4();
class Er_5b8b4 implements BeforeEachCallback, AfterEachCallback {
public void beforeEach(ExtensionContext context) {
// setup
}
public void afterEach(ExtensionContext context) {
// cleanup
}
}Changes Applied:
@Rule→@RegisterExtension@ClassRule(static) →@RegisterExtension(static)TemporaryFolder→@TempDir PathTestName→TestInfoparameter in@BeforeEachExternalResource→ Custom extension implementingBeforeEachCallback/AfterEachCallback- For class rules:
BeforeAllCallback/AfterAllCallback
The JUnit Cleanup tool automates the migration of assertions from JUnit 3 and JUnit 4 to JUnit 5.
This includes:
- Updating imports to
org.junit.jupiter.api.Assertions - Reordering parameters: JUnit 5 places the message last
- Safely transforming legacy assertion method calls
- Handling special cases like
assertThatwith Hamcrest matchers
The cleanup handles the following assertion methods from JUnit 3/4:
| Assertion Method | Parameter Count | Description |
|---|---|---|
assertEquals |
2 or 3 | Assert two values are equal |
assertNotEquals |
2 or 3 | Assert two values are not equal |
assertArrayEquals |
2 or 3 | Assert two arrays are equal |
assertSame |
2 or 3 | Assert two objects are the same |
assertNotSame |
2 or 3 | Assert two objects are not the same |
assertTrue |
1 or 2 | Assert condition is true |
assertFalse |
1 or 2 | Assert condition is false |
assertNull |
1 or 2 | Assert object is null |
assertNotNull |
1 or 2 | Assert object is not null |
fail |
0 or 1 | Explicitly fail the test |
assertThat |
2 or 3 | Assert with Hamcrest matcher |
Note: The parameter count includes the optional message parameter.
| Framework | Signature Format |
|---|---|
| JUnit 3 | assertEquals("message", expected, actual) |
| JUnit 4 | assertEquals("message", expected, actual) |
| JUnit 5 | assertEquals(expected, actual, "message") |
Note: In JUnit 5, the message is always the last argument.
The cleanup ensures correct reordering only if it's safe (i.e., the first argument is a String literal).
| JUnit 3/4 Assertion | JUnit 5 Equivalent |
|---|---|
assertEquals(expected, actual) |
assertEquals(expected, actual) |
assertEquals("msg", expected, actual) |
assertEquals(expected, actual, "msg") |
assertSame(expected, actual) |
assertSame(expected, actual) |
assertSame("msg", expected, actual) |
assertSame(expected, actual, "msg") |
assertNotSame(expected, actual) |
assertNotSame(expected, actual) |
assertNotSame("msg", expected, actual) |
assertNotSame(expected, actual, "msg") |
assertTrue(condition) |
assertTrue(condition) |
assertTrue("msg", condition) |
assertTrue(condition, "msg") |
assertFalse(condition) |
assertFalse(condition) |
assertFalse("msg", condition) |
assertFalse(condition, "msg") |
assertNull(object) |
assertNull(object) |
assertNull("msg", object) |
assertNull(object, "msg") |
assertNotNull(object) |
assertNotNull(object) |
assertNotNull("msg", object) |
assertNotNull(object, "msg") |
fail() |
fail() |
fail("msg") |
fail("msg") |
assertArrayEquals("msg", expected, actual) |
assertArrayEquals(expected, actual, "msg") |
assertNotEquals("msg", expected, actual) |
assertNotEquals(expected, actual, "msg") |
assertThat Special Handling:
| JUnit 4 Assertion | JUnit 5 Equivalent |
|---|---|
Assert.assertThat(value, matcher) |
assertThat(value, matcher) (Hamcrest) |
Assert.assertThat("msg", value, matcher) |
assertThat("msg", value, matcher) (Hamcrest) |
Note:
assertThatis migrated to use Hamcrest'sMatcherAssert.assertThatwith static import, not JUnit 5's Assertions.
Before (JUnit 3/4):
assertEquals("Expected and actual differ", expected, actual);After (JUnit 5):
assertEquals(expected, actual, "Expected and actual differ");Before:
assertNull("Object must be null", obj);After:
assertNull(obj, "Object must be null");Before:
assertTrue("Must be true", condition);
assertFalse("Must be false", condition);After:
assertTrue(condition, "Must be true");
assertFalse(condition, "Must be false");Before:
assertSame("Should be the same", expected, actual);
assertNotSame("Should not be the same", expected, actual);After:
assertSame(expected, actual, "Should be the same");
assertNotSame(expected, actual, "Should not be the same");Before:
assertNotNull("Should not be null", object);After:
assertNotNull(object, "Should not be null");Before:
fail("Unexpected state reached");After:
fail("Unexpected state reached");The cleanup also handles JUnit 4 assumption methods, which are used for conditional test execution.
The cleanup handles the following assumption methods from JUnit 4:
| Assumption Method | Parameter Count | Description |
|---|---|---|
assumeTrue |
1 or 2 | Assume condition is true |
assumeFalse |
1 or 2 | Assume condition is false |
assumeNotNull |
1 or 2 | Assume object is not null |
assumeThat |
2 or 3 | Assume with Hamcrest matcher |
| JUnit 4 Assumption | JUnit 5 Equivalent |
|---|---|
Assume.assumeTrue(condition) |
Assumptions.assumeTrue(condition) |
Assume.assumeTrue("msg", condition) |
Assumptions.assumeTrue(condition, "msg") |
Assume.assumeFalse(condition) |
Assumptions.assumeFalse(condition) |
Assume.assumeFalse("msg", condition) |
Assumptions.assumeFalse(condition, "msg") |
Assume.assumeNotNull(object) |
Assumptions.assumeNotNull(object) |
Assume.assumeNotNull("msg", object) |
Assumptions.assumeNotNull(object, "msg") |
Assume.assumeThat(value, matcher) |
assumeThat(value, matcher) (Hamcrest) |
Assume.assumeThat("msg", value, matcher) |
assumeThat("msg", value, matcher) (Hamcrest) |
Example:
Before:
import org.junit.Assume;
@Test
public void testWithAssume() {
Assume.assumeTrue("Precondition failed", true);
Assume.assumeFalse("Precondition not met", false);
Assume.assumeNotNull("Value should not be null", new Object());
}After:
import org.junit.jupiter.api.Assumptions;
@Test
public void testWithAssume() {
Assumptions.assumeTrue(true, "Precondition failed");
Assumptions.assumeFalse(false, "Precondition not met");
Assumptions.assumeNotNull(new Object(), "Value should not be null");
}Changes Applied:
org.junit.Assume→org.junit.jupiter.api.Assumptions- Parameter order changed (message moved to last position)
assumeThatuses Hamcrest's static import fromorg.hamcrest.junit.MatcherAssume
- The cleanup uses
org.junit.jupiter.api.Assertionsfor all migrated assertions - The cleanup uses
org.junit.jupiter.api.Assumptionsfor all migrated assumptions - Parameter reordering is applied conservatively, only if the first argument is a string literal
assertThatis migrated to use Hamcrest'sMatcherAssert.assertThatwith static importassumeThatis migrated to use Hamcrest'sMatcherAssume.assumeThatwith static import- Import statements are updated automatically:
org.junit.*→org.junit.jupiter.api.*org.junit.runners.*→org.junit.platform.suite.api.*- Static imports are preserved with updated package names
-
Custom Runners and Complex Rules
Tests using@RunWith(...)with custom runners, or sophisticated@Ruleimplementations may need manual migration. -
Test Suites (JUnit 3)
LegacyTestSuiteusage is automatically migrated using@TestMethodOrderand@Orderannotations to preserve test execution order. -
Parameterized Tests
JUnit 4 parameterized tests (@RunWith(Parameterized.class)) are not automatically migrated and require manual conversion to JUnit 5's@ParameterizedTest. -
Theories
JUnit 4 theories (@RunWith(Theories.class)) are not automatically migrated. -
Expected Exceptions and Timeouts
The cleanup currently does not automatically migrate@Test(expected=...)and@Test(timeout=...)attributes. These require manual conversion toassertThrows()andassertTimeout(). -
Custom Matchers
Custom Hamcrest matchers should be reviewed after migration to ensure compatibility. -
Static Imports
Both wildcard (import static org.junit.Assert.*) and explicit static imports are handled, but code style may vary.
The JUnit Cleanup can be executed from within Eclipse using the Clean Up framework.
Via Eclipse UI:
- Select Java files or packages in the Package Explorer
- Right-click → Source → Clean Up...
- Choose Configure... to customize cleanup settings
- Enable JUnit Cleanup options in the configuration
- Click Finish to apply the cleanup
Via Save Actions:
Configure automatic cleanup on save:
- Window → Preferences → Java → Editor → Save Actions
- Enable Perform the selected actions on save
- Enable Additional actions → Configure...
- Enable JUnit-related cleanup options
- Apply changes
Supported Cleanup Operations:
The JUnit Cleanup includes multiple sub-operations that can be enabled independently:
- Migrate JUnit 3 test classes to JUnit 5
- Migrate JUnit 4 annotations to JUnit 5
- Update assertion method calls (parameter reordering)
- Update assumption method calls (parameter reordering)
- Migrate
@Ruleand@ClassRuleto extensions - Update test suite configurations
- Fix method ordering annotations
Note: The cleanup is safe and non-destructive. It only transforms code that matches known patterns from JUnit 3/4 to JUnit 5 equivalents.
This documentation is based on the test coverage provided in the JUnit 3 and 4 cleanup test cases. Manual adjustments may be necessary for advanced use cases or project-specific setups.
Test Coverage:
sandbox_junit_cleanup_test/src/org/eclipse/jdt/ui/tests/quickfix/Java8/JUnit3CleanupCases.javasandbox_junit_cleanup_test/src/org/eclipse/jdt/ui/tests/quickfix/Java8/JUnitCleanupCases.javasandbox_junit_cleanup_test/src/org/eclipse/jdt/ui/tests/quickfix/Java8/JUnitMigrationCleanUpTest.java
The Method Reusability Finder is an Eclipse JDT cleanup plugin that analyzes selected methods to identify potentially reusable code patterns across the codebase. It helps developers discover duplicate or similar code that could be refactored to improve code quality and maintainability.
- Code Duplication Detection: Identify similar code patterns using both token-based and AST-based analysis
- Intelligent Matching: Recognize code similarity even when variable names differ
- Eclipse Integration: Seamlessly integrate as a cleanup action in Eclipse JDT
- Performance: Efficient analysis that scales to large codebases
- Token-based similarity: Compares normalized token sequences
- AST-based similarity: Compares abstract syntax tree structures
- Variable name normalization: Ignores variable name differences
- Control flow analysis: Matches similar control structures
- Searches method bodies for inline code sequences
- Finds code that matches a target method's body
- Identifies refactoring opportunities within methods
- Analyzes semantic safety of replacements
- Detects field modifications and side effects
- Checks for complex control flow
| Component | Purpose |
|---|---|
MethodReuseFinder |
Searches project for similar methods |
MethodSignatureAnalyzer |
Analyzes and compares method signatures |
CodePatternMatcher |
AST-based pattern matching |
InlineCodeSequenceFinder |
Finds inline code sequences |
CodeSequenceMatcher |
Matches statement sequences with normalization |
VariableMapping |
Tracks variable name mappings |
MethodCallReplacer |
Generates method invocation replacement code |
SideEffectAnalyzer |
Analyzes safety of replacements |
Cleanup options are defined in MYCleanUpConstants:
METHOD_REUSE_CLEANUP- Enable/disable the cleanupMETHOD_REUSE_INLINE_SEQUENCES- Enable inline code sequence detection
This cleanup is available as part of the JDT Clean Up framework:
- Eclipse UI → Source → Clean Up
- Automated build tools using Eclipse JDT APIs
This is a new plugin currently under development. The initial implementation focuses on:
- Basic method similarity detection
- AST-based pattern matching
- Integration with Eclipse cleanup framework
See sandbox_method_reuse/TODO.md for pending features and improvements.
The XML Cleanup plugin provides automated refactoring and optimization for PDE-relevant XML files in Eclipse projects. It focuses on reducing file size while maintaining semantic integrity through XSLT transformation, whitespace normalization, and optional indentation.
- Optimize PDE XML configuration files for size and consistency
- Apply secure XSLT transformations with whitespace normalization
- Convert leading spaces to tabs (4 spaces → 1 tab)
- Provide optional indentation control (default: OFF for size reduction)
- Integrate with Eclipse workspace APIs for safe file updates
The plugin only processes PDE-relevant XML files:
Supported File Names:
plugin.xml- Eclipse plugin manifestsfeature.xml- Eclipse feature definitionsfragment.xml- Eclipse fragment manifests
Supported File Extensions:
*.exsd- Extension point schema definitions*.xsd- XML schema definitions
Supported Locations: Files must be in one of these locations:
- Project root - Files directly in project folder
- OSGI-INF - OSGi declarative services directory
- META-INF - Manifest and metadata directory
Note: All other XML files (e.g.,
pom.xml,build.xml) are ignored to avoid unintended transformations.
- Uses secure XML processing (external DTD/entities disabled)
- Preserves XML structure, comments, and content
- Default:
indent="no"- Produces compact output for size reduction - Optional:
indent="yes"- Enabled viaXML_CLEANUP_INDENTpreference
- Reduce excessive empty lines - Maximum 2 consecutive empty lines
- Leading space to tab conversion - Only at line start (not inline text)
- Converts groups of 4 leading spaces to 1 tab
- Preserves remainder spaces (e.g., 5 spaces → 1 tab + 1 space)
- Does NOT touch inline text or content nodes
- Only writes file if content actually changed
- Uses Eclipse workspace APIs (
IFile.setContents()) - Maintains file history (
IResource.KEEP_HISTORY) - Refreshes resource after update
Default Behavior (when XML_CLEANUP is enabled):
indent="no"- Compact output, no extra whitespace- Reduces file size by removing unnecessary whitespace
- Converts leading spaces to tabs
- Preserves semantic content
Optional Behavior (when XML_CLEANUP_INDENT is enabled):
indent="yes"- Minimal indentation applied- Still converts leading spaces to tabs
- Slightly larger file size but more readable
Constants (defined in MYCleanUpConstants):
XML_CLEANUP- Enable XML cleanup (default: OFF)XML_CLEANUP_INDENT- Enable indentation (default: OFF)
The plugin implements secure XML processing:
- External DTD access disabled
- External entity resolution disabled
- DOCTYPE declarations disallowed
- Secure processing mode enabled
Tab conversion is only applied to leading whitespace:
✅ Converted:
<element> <!-- 4 leading spaces → 1 tab -->❌ Not Converted:
<element attr="value with spaces"/> <!-- Inline spaces preserved -->This ensures that:
- Indentation is normalized to tabs
- XML attribute values are not modified
- Text content spacing is preserved
- Only structural whitespace is affected
This cleanup is available as part of the JDT Clean Up framework:
- Eclipse UI → Source → Clean Up
- Configure via cleanup preferences:
XML_CLEANUPandXML_CLEANUP_INDENT
- PDE Files Only: Only processes plugin.xml, feature.xml, fragment.xml, *.exsd, *.xsd
- Location Restricted: Files must be in project root, OSGI-INF, or META-INF
- Leading Tabs Only: Tab conversion only applies to leading whitespace, not inline content
- No Schema Validation: Doesn't validate against XML schemas (relies on Eclipse PDE validation)
The sandbox_xml_cleanup_test module contains comprehensive test cases for:
- Size reduction verification
- Semantic equality (using XMLUnit, ignoring whitespace)
- Idempotency (second run produces no change)
- Leading-indent-only tab conversion
- PDE file filtering accuracy
You can use the P2 update site:
https://github.com/carstenartur/sandbox/raw/main
Warning:
Use only with a fresh Eclipse installation that can be discarded after testing.
It may break your setup. Don’t say you weren’t warned...
Contributions are welcome! This is an experimental sandbox project for testing Eclipse JDT cleanup implementations.
- Fork the repository on GitHub
- Create a feature branch from
main(the default branch):git checkout -b feature/my-new-cleanup
- Make your changes following the existing code structure and conventions
- Test your changes thoroughly:
mvn -Pjacoco verify
- Commit your changes with clear commit messages:
git commit -m "feat: add new cleanup for XYZ pattern" - Push to your fork and create a Pull Request targeting the
mainbranch
- Follow existing code patterns and cleanup structures
- Add comprehensive test cases for new cleanups
- Update documentation (README, architecture.md, todo.md) as needed
- Ensure SpotBugs, CodeQL, and all tests pass
- Keep changes focused and minimal
Found a bug or have a feature request? Please open an issue on GitHub with:
- Clear description of the problem or suggestion
- Steps to reproduce (for bugs)
- Expected vs. actual behavior
- Eclipse and Java version information
Note: This project primarily serves as an experimental playground. Features that prove stable and useful may be contributed upstream to Eclipse JDT.
This section describes how to create and publish a new release of the Sandbox project.
- Write access to the repository
- Local environment with Java 21 and Maven configured
- All tests passing on the
mainbranch
Update the version in all pom.xml files from X.Y.Z-SNAPSHOT to X.Y.Z:
# Example: Updating from 1.2.2-SNAPSHOT to 1.2.2
mvn versions:set -DnewVersion=1.2.2
mvn versions:commitEnsure all tests pass and the build completes successfully:
# Run full build with tests and coverage
mvn clean verify -Pjacoco
# Build with WAR file
mvn -Dinclude=web -Pjacoco verifyCommit the version updates:
git add .
git commit -m "Release version 1.2.2"
git push origin mainTag the release commit:
git tag -a v1.2.2 -m "Release version 1.2.2"
git push origin v1.2.2- Go to the GitHub Releases page
- Click "Draft a new release"
- Select the tag you just created (e.g.,
v1.2.2) - Set the release title (e.g.,
Release 1.2.2) - Add release notes describing:
- New features
- Bug fixes
- Breaking changes (if any)
- Known issues
- Click "Publish release"
When a GitHub release is created, the maven-publish.yml workflow automatically:
- Builds the project with Maven
- Publishes artifacts to GitHub Packages
- Makes the P2 update site available
Update versions to the next SNAPSHOT version:
# Example: Updating to 1.2.3-SNAPSHOT for next development cycle
mvn versions:set -DnewVersion=1.2.3-SNAPSHOT
mvn versions:commit
git add .
git commit -m "Prepare for next development iteration: 1.2.3-SNAPSHOT"
git push origin mainThis project follows Semantic Versioning:
- MAJOR version (X.0.0): Incompatible API changes
- MINOR version (0.X.0): New functionality in a backward-compatible manner
- PATCH version (0.0.X): Backward-compatible bug fixes
Each release produces:
- Eclipse Product: Installable Eclipse IDE with bundled plugins (
sandbox_product/target) - P2 Update Site: For installing plugins into existing Eclipse (
sandbox_web/target) - WAR File: Web-deployable update site
- Maven Artifacts: Published to GitHub Packages
Build fails during release:
- Ensure all tests pass locally:
mvn clean verify -Pjacoco - Check Java version:
java -version(must be 21+) - Verify Maven version:
mvn -version(3.9.x recommended)
GitHub Actions workflow fails:
- Check workflow run logs in the Actions tab
- Ensure the tag was pushed correctly:
git ls-remote --tags origin - Verify permissions for GitHub Packages publishing
This project is licensed under the Eclipse Public License 2.0 (EPL-2.0).
See the LICENSE.txt file for the full license text.
The Eclipse Public License (EPL) is a free and open-source software license maintained by the Eclipse Foundation. Key points:
- ✅ Commercial use allowed
- ✅ Modification allowed
- ✅ Distribution allowed
- ✅ Patent grant included
⚠️ Disclose source for modifications⚠️ License and copyright notice required
For more information, visit: https://www.eclipse.org/legal/epl-2.0/
Copyright © 2021-2025 Carsten Hammer and contributors