https://github.com/cuioss/cui-open-rewrite
OpenRewrite recipes for cuioss projects
https://github.com/cuioss/cui-open-rewrite
Last synced: 4 months ago
JSON representation
OpenRewrite recipes for cuioss projects
- Host: GitHub
- URL: https://github.com/cuioss/cui-open-rewrite
- Owner: cuioss
- License: apache-2.0
- Created: 2025-08-29T08:53:34.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2025-08-29T14:09:46.000Z (9 months ago)
- Last Synced: 2025-08-29T16:58:44.489Z (9 months ago)
- Language: Java
- Size: 17.6 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.adoc
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Security: SECURITY.md
Awesome Lists containing this project
README
= cui-open-rewrite
== Status
image:https://github.com/cuioss/cui-open-rewrite/actions/workflows/maven.yml/badge.svg[Java CI with Maven,link=https://github.com/cuioss/cui-open-rewrite/actions/workflows/maven.yml]
image:http://img.shields.io/:license-apache-blue.svg[License,link=http://www.apache.org/licenses/LICENSE-2.0.html]
image:https://img.shields.io/maven-central/v/de.cuioss.rewrite/cui-open-rewrite.svg?label=Maven%20Central["Maven Central", link="https://central.sonatype.com/artifact/de.cuioss.rewrite/cui-open-rewrite"]
https://sonarcloud.io/summary/new_code?id=cuioss_cui-open-rewrite[image:https://sonarcloud.io/api/project_badges/measure?project=cuioss_cui-open-rewrite&metric=alert_status[Quality
Gate Status]]
image:https://sonarcloud.io/api/project_badges/measure?project=cuioss_cui-open-rewrite&metric=ncloc[Lines of Code,link=https://sonarcloud.io/summary/new_code?id=cuioss_cui-open-rewrite]
image:https://sonarcloud.io/api/project_badges/measure?project=cuioss_cui-open-rewrite&metric=coverage[Coverage,link=https://sonarcloud.io/summary/new_code?id=cuioss_cui-open-rewrite]
https://cuioss.github.io/cui-java-module-template/about.html[Generated Documentation on github-pages]
== What is it?
Custom OpenRewrite recipes for CUI-OSS projects that enforce specific code formatting standards not available in existing OpenRewrite recipes.
=== Maven Coordinates
[source,xml]
----
de.cuioss.rewrite
cui-open-rewrite
----
=== 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.
[source,xml]
----
org.openrewrite.maven
rewrite-maven-plugin
org.openrewrite.java.format.AutoFormat
de.cuioss.rewrite.format.AnnotationNewlineFormat
de.cuioss.rewrite.logging.CuiLoggerStandardsRecipe
de.cuioss.rewrite.logging.CuiLogRecordPatternRecipe
de.cuioss.rewrite.logging.InvalidExceptionUsageRecipe
de.cuioss.rewrite
cui-open-rewrite
${cui-open-rewrite.version}
----
==== Run the recipe
[source,bash]
----
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
==== 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:**
[source,java]
----
// 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
[source,java]
----
// ✅ 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:
[source,java]
----
// 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:**
[source,java]
----
// 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:**
[source,java]
----
// reason
@SuppressWarnings("all")
public void method() {
----
**Best Practice for Annotation Comments:**
Instead of:
[source,java]
----
@SuppressWarnings("squid:S00107") // Number of parameters match to the use-case
public static Map mutableMap(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) {
----
Use:
[source,java]
----
// Number of parameters match to the use-case
@SuppressWarnings("squid:S00107")
public static Map 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:**
[source,java]
----
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):
[source,java]
----
// 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 https://gitingest.com/github.com/cuioss/cui-llm-rules/tree/main/standards/logging/implementation-guide.adoc[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:**
[source,java]
----
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):
[source,java]
----
// 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:**
[source,java]
----
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):
[source,java]
----
// 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
==== BaseSuppressionVisitor (Recommended)
For new recipes, extend `BaseSuppressionVisitor` to eliminate suppression code duplication:
[source,java]
----
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