Skip to content

Commit 8c57d48

Browse files
authored
Sonar codemod for defining a constant instead of duplicating literal string n times (#262)
example https://sonarcloud.io/project/issues?open=AYu2g5It97lOFaqEjR1c&id=pixee_codemodder-java reference https://rules.sonarsource.com/java/RSPEC-1192/ This codemod is approached in two ways based on the reported case by sonarcloud: 1. When sonar reports the message `Use already-defined constant 'MY_CONSTANT' instead of duplicating its value here` where we take the constant name inside the single quotes from the issue message to just replace it. 2. When sonar reports a message like `Define a constant instead of duplicating this literal "password" 3 times` where we need to generate a constant name, add it to the classas a field declarator and replace it at the reported locations. **TO GENERATE A CONSTANT NAME** We will use the string value as the constant name but there are some considerations: * We will keep only alphanumeric characters * We remove other characters that are not alphanumeric * If value has no alphanumeric characters, we will take parent's node name as its name, but if there's a case where we couldn't retrieve parent's node name, `CONST` value will be taken as the constant name * We will add a counter suffix to the variable name if there are any collisions to other declared variables in the class * Snake case is the default naming convention we are using, but before applying it, we will look for other constants declared in class to check the naming convention used, so if camel case is used, generated constant will be camel cased **New constant will be added as a `private` member of the class**
1 parent 01704e1 commit 8c57d48

31 files changed

+5316
-0
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package io.codemodder.codemods;
2+
3+
import java.util.Set;
4+
import java.util.regex.Matcher;
5+
import java.util.regex.Pattern;
6+
7+
/** This class generates constant names based on given values and parent node name. */
8+
final class ConstantNameStringGenerator {
9+
10+
private ConstantNameStringGenerator() {}
11+
12+
/**
13+
* Generates a unique constant name based on the provided string literal expression value,
14+
* declared variables, and the name of the parent node (if available). If there's a collision, a
15+
* suffix counter is added.
16+
*/
17+
static String generateConstantName(
18+
final String stringLiteralExprValue,
19+
final Set<String> declaredVariables,
20+
final String parentNodeName,
21+
final boolean isSnakeCase) {
22+
final String snakeCaseConstantName =
23+
formatStringValueToConstantSnakeCaseNomenclature(stringLiteralExprValue, parentNodeName);
24+
25+
final String constantName =
26+
isSnakeCase ? snakeCaseConstantName : convertSnakeCaseToCamelCase(snakeCaseConstantName);
27+
28+
StringBuilder constantNameBuilder = new StringBuilder(constantName);
29+
int counter = 0;
30+
31+
while (existsVariable(constantNameBuilder.toString(), declaredVariables)) {
32+
// If the constant name already exists, append a counter to make it unique
33+
constantNameBuilder = new StringBuilder(constantName);
34+
if (counter != 0) {
35+
if (isSnakeCase) {
36+
constantNameBuilder.append("_");
37+
}
38+
constantNameBuilder.append(counter);
39+
}
40+
counter++;
41+
}
42+
43+
return constantNameBuilder.toString();
44+
}
45+
46+
/**
47+
* This method takes a snake-cased string as input and transforms it into a camel-cased string. It
48+
* splits the input string by underscores, capitalizes the first letter of each subsequent word,
49+
* and concatenates the words to form the camel-cased result.
50+
*/
51+
private static String convertSnakeCaseToCamelCase(String snakeCaseString) {
52+
StringBuilder camelCaseBuilder = new StringBuilder();
53+
54+
// Split the snake case string by underscores
55+
String[] words = snakeCaseString.split("_");
56+
57+
// Append the first word with lowercase
58+
camelCaseBuilder.append(words[0].toLowerCase());
59+
60+
// Capitalize the first letter of each subsequent word and append
61+
for (int i = 1; i < words.length; i++) {
62+
String capitalizedWord =
63+
words[i].substring(0, 1).toUpperCase() + words[i].substring(1).toLowerCase();
64+
camelCaseBuilder.append(capitalizedWord);
65+
}
66+
67+
return camelCaseBuilder.toString();
68+
}
69+
70+
/**
71+
* Formats the value to be used in the constant name. The process involves removing leading
72+
* numeric characters, special characters, and spaces from the name, and converting it to
73+
* uppercase to comply with Java constant naming conventions.
74+
*/
75+
private static String formatStringValueToConstantSnakeCaseNomenclature(
76+
final String stringLiteralExprValue, final String parentNodeName) {
77+
78+
final String constName = buildName(stringLiteralExprValue, parentNodeName);
79+
80+
final String sanitizedString = sanitizeStringToOnlyAlphaNumericAndUnderscore(constName);
81+
82+
final String stringWithoutLeadingNumericCharacters =
83+
sanitizedString.replaceAll("^\\d*(_)*", "");
84+
85+
return stringWithoutLeadingNumericCharacters.toUpperCase();
86+
}
87+
88+
/**
89+
* Builds the name to be used in the constant name. It checks if the provided string literal
90+
* expression value contains only non-alphabetical characters. If it doesn't, the original value
91+
* is returned as is. Otherwise, the method combines the provided string literal expression value
92+
* with an optional prefix based on the parent node name (if available) to create a base name.
93+
*/
94+
private static String buildName(
95+
final String stringLiteralExprValue, final String parentNodeName) {
96+
97+
if (!containsOnlyNonAlpha(stringLiteralExprValue)) {
98+
return stringLiteralExprValue;
99+
}
100+
101+
return parentNodeName != null ? parentNodeName : "CONST";
102+
}
103+
104+
/** Checks if the input contains only non-alpha characters. */
105+
private static boolean containsOnlyNonAlpha(final String input) {
106+
// Use a regular expression to check if the string contains only non-alpha characters
107+
return input.matches("[^a-zA-Z]+");
108+
}
109+
110+
/** Sanitizes the input string by keeping only alphanumeric characters and underscores. */
111+
private static String sanitizeStringToOnlyAlphaNumericAndUnderscore(final String input) {
112+
// Use a regular expression to keep only alphanumeric characters and underscores
113+
final Pattern pattern = Pattern.compile("\\W");
114+
final Matcher matcher = pattern.matcher(input);
115+
116+
// Replace non-alphanumeric characters with a single space
117+
final String stringWithSpaces = matcher.replaceAll(" ");
118+
119+
// Replace consecutive spaces with a single space
120+
final String stringWithSingleSpaces = stringWithSpaces.replaceAll("\\s+", " ");
121+
122+
// Replace spaces with underscores
123+
return stringWithSingleSpaces.trim().replace(" ", "_");
124+
}
125+
126+
/**
127+
* Checks if a variable with the given constant name already exists in the declared variables set.
128+
*/
129+
private static boolean existsVariable(
130+
final String constantName, final Set<String> declaredVariables) {
131+
132+
if (declaredVariables == null || declaredVariables.isEmpty()) {
133+
return false;
134+
}
135+
136+
return declaredVariables.contains(constantName);
137+
}
138+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package io.codemodder.codemods;
2+
3+
import com.github.javaparser.ast.CompilationUnit;
4+
import com.github.javaparser.ast.Modifier;
5+
import com.github.javaparser.ast.Node;
6+
import com.github.javaparser.ast.NodeList;
7+
import com.github.javaparser.ast.body.BodyDeclaration;
8+
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
9+
import com.github.javaparser.ast.body.FieldDeclaration;
10+
import com.github.javaparser.ast.body.VariableDeclarator;
11+
import com.github.javaparser.ast.expr.StringLiteralExpr;
12+
import com.github.javaparser.ast.nodeTypes.NodeWithSimpleName;
13+
import com.github.javaparser.ast.type.ClassOrInterfaceType;
14+
import com.github.javaparser.ast.type.Type;
15+
import io.codemodder.CodemodInvocationContext;
16+
import io.codemodder.providers.sonar.api.Issue;
17+
import java.util.List;
18+
import java.util.Optional;
19+
import java.util.Set;
20+
import java.util.stream.Collectors;
21+
22+
/**
23+
* A class that extends DefineConstantForLiteral and specializes in creating new constants for
24+
* string literals in Java code.
25+
*/
26+
final class CreateConstantForLiteral extends DefineConstantForLiteral {
27+
28+
CreateConstantForLiteral(
29+
final CodemodInvocationContext context,
30+
final CompilationUnit cu,
31+
final StringLiteralExpr stringLiteralExpr,
32+
final Issue issue) {
33+
super(context, cu, stringLiteralExpr, issue);
34+
}
35+
36+
/**
37+
* Retrieves the suggested constant name from the issue's message. The method utilizes a {@link
38+
* ConstantNameStringGenerator} to generate a unique constant name based on various factors such
39+
* as the string literal value, existing names in the CompilationUnit, parent node name, and
40+
* naming conventions.
41+
*/
42+
@Override
43+
protected String getConstantName() {
44+
final List<FieldDeclaration> constantFieldDeclarations =
45+
findDeclaredConstantsInClassOrInterface();
46+
47+
final NodeWithSimpleName<?> nodeWithSimpleName = findAncestorWithSimpleName(stringLiteralExpr);
48+
49+
final String parentNodeName =
50+
nodeWithSimpleName != null ? nodeWithSimpleName.getNameAsString() : null;
51+
52+
return ConstantNameStringGenerator.generateConstantName(
53+
stringLiteralExpr.getValue(),
54+
getNamesInCu(),
55+
parentNodeName,
56+
isUsingSnakeCase(constantFieldDeclarations));
57+
}
58+
59+
/** Retrieves the names of all nodes with simple names in the CompilationUnit. */
60+
private Set<String> getNamesInCu() {
61+
return cu.findAll(Node.class).stream()
62+
.filter(node -> node instanceof NodeWithSimpleName<?>)
63+
.map(node -> (NodeWithSimpleName<?>) node)
64+
.map(NodeWithSimpleName::getNameAsString)
65+
.collect(Collectors.toSet());
66+
}
67+
68+
/** Defines a new constant by adding a {@link FieldDeclaration} to the class or interface. */
69+
@Override
70+
protected void defineConstant(final String constantName) {
71+
addConstantFieldToClass(createConstantField(constantName));
72+
}
73+
74+
/**
75+
* Adds a {@link FieldDeclaration} as the first member of the provided {@link
76+
* ClassOrInterfaceDeclaration}
77+
*/
78+
protected void addConstantFieldToClass(final FieldDeclaration constantField) {
79+
80+
final NodeList<BodyDeclaration<?>> members = classOrInterfaceDeclaration.getMembers();
81+
82+
members.addFirst(constantField);
83+
}
84+
85+
/** Creates a {@link FieldDeclaration} of {@link String} type with the constant name provided */
86+
private FieldDeclaration createConstantField(final String constantName) {
87+
88+
final NodeList<Modifier> modifiers =
89+
NodeList.nodeList(
90+
Modifier.privateModifier(), Modifier.staticModifier(), Modifier.finalModifier());
91+
92+
final Type type = new ClassOrInterfaceType(null, "String");
93+
94+
final VariableDeclarator variableDeclarator =
95+
new VariableDeclarator(
96+
type, constantName, new StringLiteralExpr(stringLiteralExpr.getValue()));
97+
98+
return new FieldDeclaration(modifiers, variableDeclarator);
99+
}
100+
101+
/**
102+
* Retrieves the first ancestor node that is a {@link NodeWithSimpleName} of a {@link
103+
* StringLiteralExpr}
104+
*/
105+
private NodeWithSimpleName<?> findAncestorWithSimpleName(
106+
final StringLiteralExpr stringLiteralExpr) {
107+
Optional<Node> parentNodeOptional = stringLiteralExpr.getParentNode();
108+
109+
while (parentNodeOptional.isPresent()
110+
&& !(parentNodeOptional.get() instanceof NodeWithSimpleName)) {
111+
parentNodeOptional = parentNodeOptional.get().getParentNode();
112+
}
113+
114+
return (NodeWithSimpleName<?>) parentNodeOptional.orElse(null);
115+
}
116+
117+
/**
118+
* This method takes a {@link ClassOrInterfaceDeclaration} as input, filters its members to
119+
* include only {@link FieldDeclaration} nodes, and further filters these FieldDeclarations to
120+
* select those that are declared as static, final, and have a type of String. The resulting list
121+
* represents the declared constants in the given class or interface.
122+
*/
123+
private List<FieldDeclaration> findDeclaredConstantsInClassOrInterface() {
124+
return classOrInterfaceDeclaration.getMembers().stream()
125+
.filter(FieldDeclaration.class::isInstance)
126+
.map(FieldDeclaration.class::cast)
127+
.filter(
128+
fieldDeclaration ->
129+
fieldDeclaration.getModifiers().contains(Modifier.staticModifier())
130+
&& fieldDeclaration.getModifiers().contains(Modifier.finalModifier())
131+
&& containsStringType(fieldDeclaration))
132+
.toList();
133+
}
134+
135+
/**
136+
* This method takes a list of {@link FieldDeclaration} objects representing constant fields. It
137+
* checks if the first constant field's name contains an underscore or is entirely in uppercase,
138+
* indicating the use of snake case naming convention. If the list is empty, the method returns
139+
* true as there are no constant fields to assess.
140+
*/
141+
private boolean isUsingSnakeCase(final List<FieldDeclaration> constantFieldDeclarations) {
142+
if (constantFieldDeclarations == null || constantFieldDeclarations.isEmpty()) {
143+
return true;
144+
}
145+
146+
final String constantName = constantFieldDeclarations.get(0).getVariable(0).getNameAsString();
147+
148+
return constantName.contains("_") || constantName.equals(constantName.toUpperCase());
149+
}
150+
151+
/**
152+
* Checks if the first constant field's name contains an underscore or is entirely in uppercase,
153+
* indicating the use of snake case naming convention.
154+
*/
155+
private boolean containsStringType(final FieldDeclaration fieldDeclaration) {
156+
for (VariableDeclarator variable : fieldDeclaration.getVariables()) {
157+
final Type fieldType = variable.getType();
158+
if (fieldType instanceof ClassOrInterfaceType classOrInterfaceType
159+
&& classOrInterfaceType.getNameAsString().equals("String")) {
160+
return true;
161+
}
162+
}
163+
return false;
164+
}
165+
}

core-codemods/src/main/java/io/codemodder/codemods/DefaultCodemods.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public static List<Class<? extends CodeChanger>> asList() {
1717
AddMissingOverrideCodemod.class,
1818
AvoidImplicitPublicConstructorCodemod.class,
1919
DeclareVariableOnSeparateLineCodemod.class,
20+
DefineConstantForLiteralCodemod.class,
2021
DisableAutomaticDirContextDeserializationCodemod.class,
2122
FixRedundantStaticOnEnumCodemod.class,
2223
HardenJavaDeserializationCodemod.class,

0 commit comments

Comments
 (0)