Skip to content

Conversation

@boraciner
Copy link

Introduce ReplaceMagicNumbersWithConstants recipe implementing Sonar rule java:S109. This recipe extracts numeric literals from method bodies into private static final constants to improve code readability and maintainability.

Also add ReplaceMagicNumbersWithConstantsTest with test cases verifying:

  • constants are generated for numeric literals
  • literals already assigned to variables or fields are ignored
  • replacements preserve correctness and type consistency

What's changed?

Added new recipe: ReplaceMagicNumbersWithConstants
Added new test class: ReplaceMagicNumbersWithConstantsTest

What's your motivation?

To enhance code quality and readability by enforcing Sonar rule java:S109, reducing the use of magic numbers in Java code.

Anything in particular you'd like reviewers to focus on?

Verify correct handling of numeric literals and naming of generated constants
Confirm no false positives for literals already assigned to variables or fields
Ensure the recipe integrates cleanly with existing static analysis infrastructure

Have you considered any alternatives or workarounds?

Manual refactoring or partial Sonar-based analysis, but these approaches lack the automation and reproducibility provided by this recipe.

Introduce `ReplaceMagicNumbersWithConstants` recipe implementing Sonar rule java:S109.
This recipe extracts numeric literals from method bodies into private static final constants
to improve code readability and maintainability.

Also add `ReplaceMagicNumbersWithConstantsTest` with test cases verifying:
- constants are generated for numeric literals
- literals already assigned to variables or fields are ignored
- replacements preserve correctness and type consistency
-1, 0, and 1 are not considered magic numbers.
-1, 0, and 1 are not considered magic numbers.
boraciner and others added 3 commits November 3, 2025 07:15
Comment on lines +18 to +19
import org.openrewrite.*;
import org.openrewrite.java.JavaTemplate;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import org.openrewrite.*;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.Cursor;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static java.util.Collections.singleton;

Comment on lines +37 to +43
public @NlsRewrite.DisplayName String getDisplayName() {
return "Replace magic numbers with constants";
}

@Override
public @NlsRewrite.Description String getDescription() {
return "Replaces magic number literals in method bodies with named constants to improve code readability and maintainability. "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public @NlsRewrite.DisplayName String getDisplayName() {
return "Replace magic numbers with constants";
}
@Override
public @NlsRewrite.Description String getDescription() {
return "Replaces magic number literals in method bodies with named constants to improve code readability and maintainability. "
public String getDisplayName() {
public String getDescription() {
return "Replaces magic number literals in method bodies with named constants to improve code readability and maintainability. " +
"Magic numbers are replaced by private static final constants declared at the top of the class, following Sonar's java:S109 rule. " +
"The recipe does not create constants for literals that are already assigned to fields or variables, nor for typical non-magic numbers (such as 0, 1, or -1). " +
"Currently, only numeric primitive literals are handled; string and character literals are unaffected. " +
"If a constant for a value already exists, or the constant name would conflict with an existing symbol, the recipe will skip that value.";

Comment on lines +68 to +70
if (!(cursor.getParent().getParent().getValue() instanceof J.VariableDeclarations.NamedVariable)
&& !isIgnoredMagicNumber(literal)) {
literals.add(literal);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!(cursor.getParent().getParent().getValue() instanceof J.VariableDeclarations.NamedVariable)
&& !isIgnoredMagicNumber(literal)) {
literals.add(literal);
if (!(cursor.getParent().getParent().getValue() instanceof J.VariableDeclarations.NamedVariable) &&
!isIgnoredMagicNumber(literal)) {

Comment on lines +129 to +130
grandparent.getValue() instanceof J.VariableDeclarations.NamedVariable)
|| isIgnoredMagicNumber(literal)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
grandparent.getValue() instanceof J.VariableDeclarations.NamedVariable)
|| isIgnoredMagicNumber(literal)) {
grandparent.getValue() instanceof J.VariableDeclarations.NamedVariable) ||
isIgnoredMagicNumber(literal)) {

Comment on lines +145 to +156
private String printCursorPath(Cursor cursor) {
StringBuilder sb = new StringBuilder();
while (cursor != null) {
Object val = cursor.getValue();
sb.append(val == null ? "null" : val.getClass().getSimpleName());
sb.append(" > ");
cursor = cursor.getParent();
}
sb.append("ROOT");
return sb.toString();
}
@Override
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private String printCursorPath(Cursor cursor) {
StringBuilder sb = new StringBuilder();
while (cursor != null) {
Object val = cursor.getValue();
sb.append(val == null ? "null" : val.getClass().getSimpleName());
sb.append(" > ");
cursor = cursor.getParent();
}
sb.append("ROOT");
return sb.toString();
}
@Override
return singleton("RSPEC-109");

private String getStrValFromLiteral(J.Literal literal) {
String type = getTypeName(literal).toUpperCase();
String valueSource = literal.getValueSource();
if (valueSource == null) return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (valueSource == null) return null;
if (valueSource == null) {
return null;
}

}

private String getTypeName(J.Literal literal) {
if (literal.getType() == null) return "Object";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (literal.getType() == null) return "Object";
if (literal.getType() == null) {
return "Object";
}

Comment on lines +34 to +35
import org.openrewrite.java.JavaParser;
import org.openrewrite.test.RecipeSpec;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import org.openrewrite.java.JavaParser;
import org.openrewrite.test.RecipeSpec;
class ReplaceMagicNumbersWithConstantsTest implements RewriteTest {

Comment on lines +107 to +108
@DocumentExample
@Test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@DocumentExample
@Test
@Test

Comment on lines +136 to +137
}
@DocumentExample
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
@DocumentExample
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants