Custom OpenRewrite recipes for CUI-OSS projects that enforce specific code formatting standards not available in existing OpenRewrite recipes.
The recipes run on both JDK 21 and JDK 25. On JDK 21 the rewrite-java-21 parser is used; on JDK 25 the rewrite-java-25 parser is activated automatically via the java25-parser Maven profile.
Replace ${cui-open-rewrite.version} below with the latest release (see the Maven Central badge above).
<dependency>
<groupId>de.cuioss.rewrite</groupId>
<artifactId>cui-open-rewrite</artifactId>
<version>${cui-open-rewrite.version}</version>
</dependency>When combining with AutoFormat, run AutoFormat FIRST, then CUI recipes. AutoFormat includes NormalizeFormat which can undo annotation formatting if the order is reversed.
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<configuration>
<activeRecipes>
<!-- Base formatting FIRST -->
<recipe>org.openrewrite.java.format.AutoFormat</recipe>
<!-- CUI recipes AFTER -->
<recipe>de.cuioss.rewrite.format.AnnotationNewlineFormat</recipe>
<recipe>de.cuioss.rewrite.logging.CuiLoggerStandardsRecipe</recipe>
<recipe>de.cuioss.rewrite.logging.CuiLogRecordPatternRecipe</recipe>
<recipe>de.cuioss.rewrite.logging.InvalidExceptionUsageRecipe</recipe>
</activeRecipes>
</configuration>
<dependencies>
<dependency>
<groupId>de.cuioss.rewrite</groupId>
<artifactId>cui-open-rewrite</artifactId>
<version>${cui-open-rewrite.version}</version>
</dependency>
</dependencies>
</plugin>When recipes detect issues that cannot be auto-fixed, they add SearchResult markers following OpenRewrite standards:
-
Markers appear as
/~(Message)~>/comments in the source code -
The message often starts with "TODO:" to indicate required manual action
-
These markers are visible in the modified source files after running
mvn rewrite:run -
This is standard OpenRewrite behavior for marking code that needs attention
-
To remove markers, either fix the underlying issue or suppress the recipe
In addition to the in-source markers, every marker-producing recipe emits one
WARN-level build-log line per finding on each run. This makes findings visible in the
build log even when the marker was already present in the source (a committed marker
produces no diff, so the in-source marker alone would go unnoticed on a re-run):
-
Each WARN line carries the source file path, the line and column of the flagged element, the recipe name, and the marker/task message
-
Newly-detected findings and pre-existing findings use distinguishable wording (
Finding detectedvsFinding pre-existing) so the two cases are independently greppable -
The WARN lines are emitted through
CuiLoggerand appear on the build console regardless of whether the run changed the source
Ensures that Java annotations are formatted on separate lines for better readability.
Effect:
-
Class-level annotations (e.g.,
@UtilityClass,@Slf4j) are placed on separate lines -
Method-level annotations (e.g.,
@Override,@Test) are placed on separate lines -
Field-level annotations are placed on separate lines
-
Multiple annotations are each placed on their own line
Example transformation:
// Before
@Data @Builder public class Person {
@Override public String toString() {
return "Person";
}
}
// After
@Data
@Builder
public class Person {
@Override
public String toString() {
return "Person";
}
}Suppression:
Recipes can be suppressed using comments:
-
// cui-rewrite:disable- Suppresses all recipes for the next element -
// cui-rewrite:disable AnnotationNewlineFormat- Suppresses this specific recipe only
Due to how OpenRewrite attaches comments to AST nodes, the position of suppression comments is critical:
-
For elements with annotations: Place the suppression comment BEFORE the first annotation, not between annotations and the declaration
-
For elements without annotations: Place the suppression comment directly before the element
// ✅ CORRECT: Comment before annotations
// cui-rewrite:disable
@Data
@Builder
public class Person {
// ✅ CORRECT: Comment before annotation
// cui-rewrite:disable AnnotationNewlineFormat
@Override
@Test
public void test() {}
// ✅ CORRECT: No annotations, comment directly before element
// cui-rewrite:disable
public void simpleMethod() {}
}
// ❌ WRONG: Comment between annotations and class
@Data
@Builder
// cui-rewrite:disable // This won't work!
public class Person {
// ❌ WRONG: Comment between annotations
@Override
// cui-rewrite:disable // This won't work!
@Test
public void test() {}
}Class-Level Suppression:
When a class has a suppression comment, it applies to all elements within that class:
// cui-rewrite:disable
@Data
public class Person {
// All methods, fields, and nested classes are suppressed
private String name;
public void method() {}
}Known Limitations:
-
Trailing comments (
public class Foo { // cui-rewrite:disable) not fully supported due to OpenRewrite AST limitations -
Classes without modifiers: Package-private classes with no modifiers (e.g.,
class Foo) cannot be formatted due to OpenRewrite AST structure -
Trailing comments on annotations are moved to the next line - Comments like
@SuppressWarnings("all") // reasonwill be preserved but repositioned-
This is due to OpenRewrite’s AST model where inline comments are stored in the prefix of the NEXT element
-
When reformatting adds a newline, the comment moves with it
-
The comment is NOT lost, just repositioned to the line after the annotation with proper indentation
-
Recommended solution: Place comments on the line above annotations rather than inline
-
Example of comment movement:
// Before
@SuppressWarnings("all") // reason
public void method() {
// After (comment moved to next line with proper indentation)
@SuppressWarnings("all")
// reason
public void method() {Recommended approach to avoid comment repositioning:
// reason
@SuppressWarnings("all")
public void method() {Best Practice for Annotation Comments:
Instead of:
@SuppressWarnings("squid:S00107") // Number of parameters match to the use-case
public static Map<K, V> mutableMap(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) {Use:
// Number of parameters match to the use-case
@SuppressWarnings("squid:S00107")
public static Map<K, V> mutableMap(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) {Enforces CUI-specific logging standards including proper logger naming, string substitution patterns, exception parameter positioning, and detection of System.out/System.err usage.
Auto-fixes:
-
Logger naming - Renames logger fields to
LOGGER(uppercase) -
Logger modifiers - Fixes loggers to be
private static final -
Placeholder patterns - Replaces
{}(SLF4J) and%d,%f, etc. with%s -
Exception positioning - Moves exception parameters to first position for error/warn calls
Marks for review (with SearchResult markers):
-
System.out/err usage - Adds markers to flag inappropriate console output
-
Parameter count mismatch - Adds markers when placeholder count doesn’t match parameter count
Example detections:
public class Example {
public static CuiLogger log = new CuiLogger(Example.class); // ⚠️ Should be 'LOGGER' and 'private static final'
void method(Exception e) {
System.out.println("Debug message"); // ⚠️ Use proper logging
log.error("Error {} occurred", "Database", e); // ⚠️ Wrong placeholder and exception position
log.info("Count: %d", 42); // ⚠️ Should use %s
}
}Suppression:
To suppress this specific recipe, use // cui-rewrite:disable CuiLoggerStandardsRecipe (see Comment Positioning for Suppression section above for critical placement rules):
// cui-rewrite:disable CuiLoggerStandardsRecipe
System.out.println("This won't be flagged");
// cui-rewrite:disable CuiLoggerStandardsRecipe
public CuiLogger logger = new CuiLogger(Example.class); // Won't be changed
// For methods/classes with annotations, place before the first annotation:
// cui-rewrite:disable CuiLoggerStandardsRecipe
@Test
public void testMethod() {
System.out.println("Suppressed");
}Enforces proper usage of LogRecord pattern according to CUI logging standards:
-
Mandatory for INFO/WARN/ERROR/FATAL levels - Direct logging is not allowed
-
Forbidden for DEBUG/TRACE levels - Must use direct logging instead
See the Logging Implementation Guide for details.
Auto-fixes:
-
LogRecord template placeholders - Replaces incorrect placeholders (
{},%d,%f, etc.) with%sin LogRecord templates -
Zero-parameter format() to method reference - Converts
LogRecord.format()calls with no parameters to method references (LogRecord::format)
Marks for review (with SearchResult markers):
-
Missing LogRecord - Adds
/~~(TODO: INFO/WARN/ERROR/FATAL needs LogRecord)~~>/markers to calls without LogRecord -
Inappropriate LogRecord - Adds
/~~(TODO: DEBUG/TRACE no LogRecord)~~>/markers to calls using LogRecord -
Note: These markers follow OpenRewrite standards and will appear in the source code when using
rewrite:run
Example detections:
public class Example {
private static final CuiLogger LOGGER = new CuiLogger(Example.class);
// LogMessage constants
static class INFO {
static final LogRecord USER_LOGIN = LogRecordModel.builder()
.template("User %s logged in")
.build();
}
void goodExamples() {
// ✅ Correct: LogRecord for INFO level
LOGGER.info(INFO.USER_LOGIN.format(username));
// ✅ Correct: Method reference for zero-parameter format
LOGGER.info(INFO.APPLICATION_STARTED::format);
// ✅ Correct: Direct logging for DEBUG level
LOGGER.debug("Processing file %s", filename);
}
void badExamples() {
// ⚠️ Wrong: Direct logging for INFO level
LOGGER.info("User %s logged in", username);
// ⚠️ Wrong: LogRecord for DEBUG level
LOGGER.debug(DEBUG.SOME_MESSAGE.format());
// 🔧 Auto-fixed: Zero-parameter format() converted to method reference
LOGGER.info(INFO.SIMPLE_MESSAGE.format()); // → INFO.SIMPLE_MESSAGE::format
}
}Suppression:
To suppress this specific recipe, use // cui-rewrite:disable CuiLogRecordPatternRecipe (see Comment Positioning for Suppression section above for critical placement rules):
// cui-rewrite:disable CuiLogRecordPatternRecipe
LOGGER.info("Direct logging suppressed for this call");
// For annotated methods, place before the first annotation:
// cui-rewrite:disable CuiLogRecordPatternRecipe
@Test
public void testMethod() {
LOGGER.info("Direct logging allowed here");
}Flags inappropriate usage of generic exception types that should be replaced with specific exceptions for better error handling and debugging.
Marks for review (with SearchResult markers):
-
Catching generic exceptions - Adds markers to catch blocks using
Exception,RuntimeException, orThrowable -
Throwing generic exceptions - Adds markers to throw statements with
new Exception(),new RuntimeException(), ornew Throwable() -
Creating generic exceptions - Adds markers to instantiation of generic exception types even when not immediately thrown
-
Note: These issues require manual review to choose appropriate specific exception types
Why no auto-fix?
The appropriate specific exception type depends on the context and business logic. Manual review is required to choose the correct exception type.
Example detections:
public class Example {
void badExamples() {
try {
doSomething();
} catch (Exception e) { // ⚠️ Too generic - use specific exception
log.error("Error occurred", e);
}
try {
riskyOperation();
} catch (RuntimeException e) { // ⚠️ Too generic
throw new Exception("Wrapped", e); // ⚠️ Throwing generic exception
}
try {
doIO();
} catch (Throwable t) { // ⚠️ Never catch Throwable
// Handle
}
}
void goodExamples() throws IOException {
try {
doSomething();
} catch (IOException e) { // ✅ Specific exception type
log.error("IO error", e);
} catch (IllegalArgumentException e) { // ✅ Specific runtime exception
throw new ValidationException("Invalid input", e); // ✅ Domain-specific exception
}
}
}Suppression:
To suppress this specific recipe, use // cui-rewrite:disable InvalidExceptionUsageRecipe (see Comment Positioning for Suppression section above for critical placement rules):
// cui-rewrite:disable InvalidExceptionUsageRecipe
catch (Exception e) { // Suppressed for this specific case
// Legacy code that needs generic catch
}
// cui-rewrite:disable InvalidExceptionUsageRecipe
throw new RuntimeException("Suppressed generic exception");
// For annotated methods, place before the first annotation:
// cui-rewrite:disable InvalidExceptionUsageRecipe
@Test
public void testMethod() throws Exception {
// Generic exception allowed in test
}For new recipes, extend BaseSuppressionVisitor to eliminate suppression code duplication:
import de.cuioss.rewrite.util.BaseSuppressionVisitor;
public class YourRecipe extends Recipe {
public static final String RECIPE_NAME = "YourRecipeName";
private static class YourRecipeVisitor extends BaseSuppressionVisitor {
public YourRecipeVisitor() {
super(RECIPE_NAME);
}
// Class and method level suppression is handled automatically
// For element-level suppression, use the convenient helper method
@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
J.MethodInvocation mi = super.visitMethodInvocation(method, ctx);
// Check element-level suppression using the convenient helper
if (isSuppressed()) {
return mi;
}
// Your recipe logic here
// Example: if (someCondition) { mi = mi.withSomeModification(...); }
return mi;
}
}
}Benefits:
-
Eliminates code duplication for class and method level suppression (handled automatically)
-
Provides convenient
isSuppressed()helper method instead of verboseRecipeSuppressionUtil.isSuppressed(getCursor(), recipeName) -
Consistent suppression behavior across all recipes
-
Reduces suppression boilerplate while maintaining full recipe functionality
Design Rationale:
Complete automatic suppression is not feasible because many recipes need to process elements before determining whether they should be suppressed. For example, a recipe might need to:
-
Process a method invocation to determine if it should be marked with a warning
-
Check business logic conditions before applying suppression
-
Perform transformations that depend on the element’s content
BaseSuppressionVisitor strikes the optimal balance by:
-
Automatically handling class/method suppression (which always works the same way)
-
Providing a clean helper for element-level suppression (which varies by recipe logic)
-
Eliminating the original code duplication that triggered the Sonar warning