Skip to content

cuioss/cui-open-rewrite

cui-open-rewrite

What is it?

Custom OpenRewrite recipes for CUI-OSS projects that enforce specific code formatting standards not available in existing OpenRewrite recipes.

Requirements

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.

Maven Coordinates

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>

Usage

With OpenRewrite Maven Plugin

⚠️ CRITICAL: Recipe Execution Order

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>

Run the recipe

mvn rewrite:run

Features

Important Note on Detection Markers

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 detected vs Finding pre-existing) so the two cases are independently greppable

  • The WARN lines are emitted through CuiLogger and appear on the build console regardless of whether the run changed the source

AnnotationNewlineFormat Recipe

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

⚠️ IMPORTANT: Comment Positioning for Suppression

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") // reason will 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) {

CuiLoggerStandardsRecipe

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");
}

CuiLogRecordPatternRecipe

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 %s in 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");
}

InvalidExceptionUsageRecipe

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, or Throwable

  • Throwing generic exceptions - Adds markers to throw statements with new Exception(), new RuntimeException(), or new 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 Recipe Developers

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 verbose RecipeSuppressionUtil.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:

  1. Automatically handling class/method suppression (which always works the same way)

  2. Providing a clean helper for element-level suppression (which varies by recipe logic)

  3. Eliminating the original code duplication that triggered the Sonar warning

About

OpenRewrite recipes for cuioss projects

Resources

License

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Sponsor this project

Contributors

Languages