All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openrewrite.java.VariableNameUtils Maven / Gradle / Ivy

There is a newer version: 8.40.2
Show newest version
/*
 * Copyright 2022 the original author or authors.
 * 

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* https://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.openrewrite.java; import lombok.Value; import org.jspecify.annotations.Nullable; import org.openrewrite.Cursor; import org.openrewrite.Incubating; import org.openrewrite.java.tree.*; import java.text.Normalizer; import java.util.*; @Incubating(since = "7.25.0") @Value public class VariableNameUtils { private VariableNameUtils() { } /** * Generates a variable name without namespace conflicts in the scope of a cursor. *

* A JavaSourceFile must be available in the Cursor path to account for all names * available in the cursor scope. *

* Since the client recipe may have modified the LST element represented by {@code scope}, this method will * visit the value held by {@code scope} once it encounters it in the tree. For this to work, the client recipe * must use {@link Cursor#Cursor(Cursor, Object)} to pass a cursor holding the modified LST element. * * @param baseName baseName for the variable. * @param scope The cursor position of a JavaVisitor, {@link org.openrewrite.java.JavaVisitor#getCursor()}. * @param strategy {@link GenerationStrategy} to use if a name already exists with the baseName. * @return either the baseName if a namespace conflict does not exist or a name generated with the provided strategy. */ public static String generateVariableName(String baseName, Cursor scope, GenerationStrategy strategy) { Set namesInScope = findNamesInScope(scope); // Generate a new name to prevent namespace shadowing. String newName = baseName; if (GenerationStrategy.INCREMENT_NUMBER.equals(strategy)) { StringBuilder postFix = new StringBuilder(); char[] charArray = baseName.toCharArray(); for (int i = charArray.length - 1; i >= 0; i--) { char c = charArray[i]; if (Character.isDigit(c)) { postFix.append(c); } else { break; } } baseName = baseName.substring(0, baseName.length() - postFix.length()); int count = postFix.length() == 0 ? 0 : Integer.parseInt(postFix.reverse().toString()); while (namesInScope.contains(newName) || JavaKeywords.isReserved(newName)) { newName = baseName + (count += 1); } } return newName; } /** * Replace accent and diacritics with normalized characters. * @param name variable name to normalize. * @return normalized name. */ public static String normalizeName(String name) { if (name.isEmpty() || Normalizer.isNormalized(name, Normalizer.Form.NFKD)) { return name; } String normalized = Normalizer.normalize(name, Normalizer.Form.NFKD); return normalized.replaceAll("\\p{M}", ""); } /** * Find the names of variables that already exist in the scope of a cursor. * A JavaSourceFile must be available in the Cursor path to account for all names * available in the cursor scope. * * @param scope The cursor position of a JavaVisitor, {@link org.openrewrite.java.JavaVisitor#getCursor()}. * @return Variable names available in the name scope of the cursor. */ public static Set findNamesInScope(Cursor scope) { JavaSourceFile compilationUnit = scope.firstEnclosing(JavaSourceFile.class); if (compilationUnit == null) { throw new IllegalStateException("A JavaSourceFile is required in the cursor path."); } Set names = new HashSet<>(); VariableNameScopeVisitor variableNameScopeVisitor = new VariableNameScopeVisitor(scope); variableNameScopeVisitor.visit(compilationUnit, names); return names; } /** * Collects class field names inherited from super classes. *

* Note: Does not currently account for {@link JavaType.Unknown}. */ public static Set findInheritedNames(J.ClassDeclaration classDeclaration) { Set names = new HashSet<>(); if (classDeclaration.getType() != null) { addInheritedClassFields(classDeclaration, classDeclaration.getType().getSupertype(), names); } return names; } private static void addInheritedClassFields(J.ClassDeclaration classDeclaration, JavaType.@Nullable FullyQualified superClass, Set names) { if (superClass != null) { boolean isSamePackage = classDeclaration.getType() != null && classDeclaration.getType().getPackageName().equals(superClass.getPackageName()); superClass.getMembers().forEach(m -> { if ((Flag.hasFlags(m.getFlagsBitMap(), Flag.Public) || Flag.hasFlags(m.getFlagsBitMap(), Flag.Protected)) || // Member is accessible as package-private. !Flag.hasFlags(m.getFlagsBitMap(), Flag.Private) && isSamePackage) { names.add(m.getName()); } }); addInheritedClassFields(classDeclaration, superClass.getSupertype(), names); } } public enum GenerationStrategy { INCREMENT_NUMBER } private static final class VariableNameScopeVisitor extends JavaIsoVisitor> { private final Cursor scope; private final Map> nameScopes; private final Stack currentScope; public VariableNameScopeVisitor(Cursor scope) { this.scope = scope; this.nameScopes = new LinkedHashMap<>(); this.currentScope = new Stack<>(); } /** * Aggregates namespaces into specific scopes. * I.E. names declared in a {@link J.ControlParentheses} will belong to the parent AST element. */ private Cursor aggregateNameScope() { return getCursor().dropParentUntil(is -> is instanceof JavaSourceFile || is instanceof J.ClassDeclaration || is instanceof J.MethodDeclaration || is instanceof J.Block || is instanceof J.ForLoop || is instanceof J.ForEachLoop || is instanceof J.Case || is instanceof J.Try || is instanceof J.Try.Catch || is instanceof J.Lambda); } @Override public Statement visitStatement(Statement statement, Set namesInScope) { Statement s = super.visitStatement(statement, namesInScope); Cursor aggregatedScope = aggregateNameScope(); if (currentScope.isEmpty() || currentScope.peek() != aggregatedScope) { Set namesInAggregatedScope = nameScopes.computeIfAbsent(aggregatedScope, k -> new HashSet<>()); // Pass the name scopes available from a parent scope down to the child. if (!currentScope.isEmpty() && aggregatedScope.isScopeInPath(currentScope.peek().getValue())) { namesInAggregatedScope.addAll(nameScopes.get(currentScope.peek())); } currentScope.push(aggregatedScope); } return s; } @Override public @Nullable J preVisit(J tree, Set namesInScope) { // visit value from scope rather than `tree`, since calling recipe may have modified it already return scope. getValue().isScope(tree) ? scope.getValue() : super.preVisit(tree, namesInScope); } // Stop after the tree has been processed to ensure all the names in scope have been collected. @Override public @Nullable J postVisit(J tree, Set namesInScope) { if (!currentScope.isEmpty() && currentScope.peek().getValue().equals(tree)) { currentScope.pop(); } if (scope.getValue().equals(tree)) { Cursor aggregatedScope = getCursor().getValue() instanceof JavaSourceFile ? getCursor() : aggregateNameScope(); // Add names from parent scope. Set names = nameScopes.get(aggregatedScope); // Add the names created in the target scope. namesInScope.addAll(names); nameScopes.forEach((key, value) -> { if (key.isScopeInPath(scope.getValue())) { namesInScope.addAll(value); } }); return tree; } return super.postVisit(tree, namesInScope); } @Override public J.Import visitImport(J.Import _import, Set namesInScope) { // Skip identifiers from `import`s. return _import; } @Override public J.Package visitPackage(J.Package pkg, Set namesInScope) { // Skip identifiers from `package`. return pkg; } @Override public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, Set namesInScope) { // Collect class fields first, because class fields are always visible regardless of what order the statements are declared. classDecl.getBody().getStatements().forEach(o -> { if (o instanceof J.VariableDeclarations) { J.VariableDeclarations variableDeclarations = (J.VariableDeclarations) o; variableDeclarations.getVariables().forEach(v -> nameScopes.computeIfAbsent(getCursor(), k -> new HashSet<>()).add(v.getSimpleName())); } }); addImportedStaticFieldNames(getCursor().firstEnclosing(JavaSourceFile.class), getCursor()); if (classDecl.getType() != null) { namesInScope.addAll(findInheritedNames(classDecl)); } return super.visitClassDeclaration(classDecl, namesInScope); } @Override public J.InstanceOf visitInstanceOf(J.InstanceOf instanceOf, Set strings) { if (instanceOf.getPattern() instanceof J.Identifier) { Set names = nameScopes.get(currentScope.peek()); if (names != null) { names.add(((J.Identifier)instanceOf.getPattern()).getSimpleName()); } } return super.visitInstanceOf(instanceOf, strings); } @Override public J.VariableDeclarations.NamedVariable visitVariable(J.VariableDeclarations.NamedVariable variable, Set strings) { Set names = nameScopes.get(currentScope.peek()); if (names != null) { names.add(variable.getSimpleName()); } return super.visitVariable(variable, strings); } private void addImportedStaticFieldNames(@Nullable JavaSourceFile cu, Cursor classCursor) { if (cu != null) { List imports = cu.getImports(); imports.forEach(i -> { if (i.isStatic()) { // Note: Currently, adds all statically imported identifiers including method and classes rather than restricting the names to static fields. Set namesAtCursor = nameScopes.computeIfAbsent(classCursor, k -> new HashSet<>()); if (isValidImportName(i.getQualid().getTarget().getType(), i.getQualid().getSimpleName())) { namesAtCursor.add(i.getQualid().getSimpleName()); } } }); } } private boolean isValidImportName(@Nullable JavaType targetType, String name) { // Consider the id a valid field if the type is null since it is indistinguishable from a method name or class name. return targetType == null || (targetType instanceof JavaType.FullyQualified && ((JavaType.FullyQualified) targetType).getMembers().stream().anyMatch(o -> o.getName().equals(name))); } } static final class JavaKeywords { JavaKeywords() { } private static final String[] RESERVED_WORDS = new String[]{ "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", "volatile", "while", }; private static final Set RESERVED_WORDS_SET = new HashSet<>(Arrays.asList(RESERVED_WORDS)); public static boolean isReserved(String word) { return RESERVED_WORDS_SET.contains(word); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy