ChangeBehavior.java
/*******************************************************************************
* Copyright (c) 2021 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper;
import static org.sandbox.jdt.internal.common.LibStandardNames.METHOD_DEFAULT_CHARSET;
import static org.sandbox.jdt.internal.common.LibStandardNames.METHOD_DISPLAY_NAME;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.Initializer;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.TypeParameter;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.ScopeAnalyzer;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
public enum ChangeBehavior {
KEEP_BEHAVIOR() {
@Override
protected Expression computeCharsetASTNode(final CompilationUnitRewrite cuRewrite, AST ast, ASTNode visited,
String charset, Map<String, QualifiedName> charsetConstants) {
Expression callToCharsetDefaultCharset= null;
if (charset != null) {
callToCharsetDefaultCharset= addCharsetUTF8(cuRewrite, ast, charset);
} else {
// needs Java 1.5
callToCharsetDefaultCharset= addCharsetComputation(cuRewrite, ast);
}
return callToCharsetDefaultCharset;
}
@Override
protected String computeCharsetforPreview() {
String insert= "Charset.defaultCharset()"; //$NON-NLS-1$
return insert;
}
},
ENFORCE_UTF8() {
@Override
protected Expression computeCharsetASTNode(final CompilationUnitRewrite cuRewrite, AST ast, ASTNode visited,
String charset, Map<String, QualifiedName> charsetConstants) {
String charset2= charset == null ? "UTF_8" : charset; //$NON-NLS-1$
Expression callToCharsetDefaultCharset= addCharsetUTF8(cuRewrite, ast, charset2);
return callToCharsetDefaultCharset;
}
@Override
protected String computeCharsetforPreview() {
String insert= "StandardCharsets.UTF_8"; //$NON-NLS-1$
return insert;
}
},
ENFORCE_UTF8_AGGREGATE() {
@Override
protected Expression computeCharsetASTNode(final CompilationUnitRewrite cuRewrite, AST ast, ASTNode visited,
String charset2, Map<String, QualifiedName> charsetConstants) {
String charset= charset2 == null ? "UTF_8" : charset2; //$NON-NLS-1$
TypeDeclaration enclosingType= findAggregateCharsetOwner(cuRewrite, visited);
if (enclosingType == null) {
return addCharsetUTF8(cuRewrite, ast, normalizeCharsetFieldName(charset));
}
String ownerKey= aggregateCharsetOwnerKey(cuRewrite.getRoot(), enclosingType);
String normalizedCharset= normalizeCharsetFieldName(charset);
String reservationKey= ownerKey + '\u0000' + normalizedCharset;
VariableDeclarationFragment existingField= findCompatibleCharsetField(enclosingType, normalizedCharset);
if (existingField != null ? !isSafeToReadField(enclosingType, existingField, visited)
: !canInitializeOwnFieldsFirst(enclosingType)) {
return addCharsetUTF8(cuRewrite, ast, normalizedCharset);
}
String ownerName= aggregateOwnerName(cuRewrite.getRoot(), enclosingType, visited);
if (ownerName == null) {
return addCharsetUTF8(cuRewrite, ast, normalizedCharset);
}
QualifiedName cached= charsetConstants.get(reservationKey);
if (cached != null) {
// Reservations are owner-local; expression qualifiers are occurrence-local.
return ast.newQualifiedName(ast.newName(ownerName),
ast.newSimpleName(cached.getName().getIdentifier()));
}
String fieldName= existingField != null
? existingField.getName().getIdentifier()
: generateCharsetFieldName(enclosingType, ownerKey, normalizedCharset, charsetConstants);
QualifiedName fieldReference= ast.newQualifiedName(
ast.newName(ownerName), ast.newSimpleName(fieldName));
if (existingField == null) {
ImportRewrite importRewrite= cuRewrite.getImportRewrite();
String standardCharsetsType= importRewrite.addImport(StandardCharsets.class.getCanonicalName());
String charsetType= importRewrite.addImport(Charset.class.getCanonicalName());
FieldAccess initializer= ast.newFieldAccess();
initializer.setExpression(ast.newName(standardCharsetsType));
initializer.setName(ast.newSimpleName(normalizedCharset));
VariableDeclarationFragment fragment= ast.newVariableDeclarationFragment();
fragment.setName(ast.newSimpleName(fieldName));
fragment.setInitializer(initializer);
FieldDeclaration fieldDeclaration= ast.newFieldDeclaration(fragment);
fieldDeclaration.setType(ast.newSimpleType(ast.newName(charsetType)));
fieldDeclaration.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.PRIVATE_KEYWORD));
fieldDeclaration.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.STATIC_KEYWORD));
fieldDeclaration.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.FINAL_KEYWORD));
cuRewrite.getASTRewrite().getListRewrite(enclosingType, TypeDeclaration.BODY_DECLARATIONS_PROPERTY)
.insertFirst(fieldDeclaration, null);
}
charsetConstants.put(reservationKey, fieldReference);
return copyQualifiedName(ast, fieldReference);
}
@Override
protected String computeCharsetforPreview() {
return "CharsetConstant"; //$NON-NLS-1$
}
};
abstract protected Expression computeCharsetASTNode(CompilationUnitRewrite cuRewrite, AST ast, ASTNode visited,
String charset, Map<String, QualifiedName> charsetConstants);
abstract protected String computeCharsetforPreview();
protected static FieldDeclaration findStaticCharsetField(TypeDeclaration type, String fieldName) {
for (FieldDeclaration field : type.getFields()) {
for (Object fragment : field.fragments()) {
if (fragment instanceof VariableDeclarationFragment varFrag
&& varFrag.getName().getIdentifier().equals(fieldName)) {
return field;
}
}
}
return null;
}
protected static Expression createCharsetAccessExpression(AST ast, String charset) {
FieldAccess fieldAccess= ast.newFieldAccess();
fieldAccess.setExpression(ast.newName(StandardCharsets.class.getSimpleName()));
fieldAccess.setName(ast.newSimpleName(charset));
return fieldAccess;
}
/**
* Create access to StandardCharsets.UTF_8, needs Java 1.7 or newer
*
* @param cuRewrite CompilationUnitRewrite
* @param ast AST
* @param charset Charset as String
* @return FieldAccess that returns Charset for UTF_8
*/
protected static FieldAccess addCharsetUTF8(CompilationUnitRewrite cuRewrite, AST ast, String charset) {
/**
* Add import java.nio.charset.StandardCharsets - available since Java 1.7
*/
ImportRewrite importRewrite= cuRewrite.getImportRewrite();
String standardCharsetsType= importRewrite.addImport(StandardCharsets.class.getCanonicalName());
/**
* Add field access to StandardCharsets.UTF_8
*/
FieldAccess fieldaccess= ast.newFieldAccess();
fieldaccess.setExpression(ASTNodeFactory.newName(ast, standardCharsetsType));
fieldaccess.setName(ast.newSimpleName(charset));
return fieldaccess;
}
/**
* Create call to Charset.defaultCharset(), needs Java 1.5 or newer
*
* @param cuRewrite CompilationUnitRewrite
* @param ast AST
* @return MethodInvocation that returns Charset for platform encoding
*/
protected static MethodInvocation addCharsetComputation(final CompilationUnitRewrite cuRewrite, AST ast) {
/**
* Add import java.nio.charset.Charset
*/
ImportRewrite importRewrite= cuRewrite.getImportRewrite();
importRewrite.addImport(Charset.class.getCanonicalName());
/**
* Create call to Charset.defaultCharset() - this is available since Java 1.5
*/
MethodInvocation firstCall= ast.newMethodInvocation();
firstCall.setExpression(ASTNodeFactory.newName(ast, Charset.class.getSimpleName()));
firstCall.setName(ast.newSimpleName(METHOD_DEFAULT_CHARSET));
return firstCall;
}
/**
* Create call to Charset.defaultCharset().displayName(), needs Java 1.5 or newer
*
* @param cuRewrite CompilationUnitRewrite
* @param ast AST
* @param cb ChangeBehavior
* @param charset Charset as String
* @return MethodInvocation that returns String
*/
protected MethodInvocation addCharsetStringComputation(final CompilationUnitRewrite cuRewrite, AST ast,
ASTNode visited, ChangeBehavior cb, String charset, Map<String, QualifiedName> charsetConstants) {
Expression callToCharsetDefaultCharset= computeCharsetASTNode(cuRewrite, ast, visited, charset, charsetConstants);
/**
* Add second call to Charset.defaultCharset().displayName()
*/
MethodInvocation secondCall= ast.newMethodInvocation();
secondCall.setExpression(callToCharsetDefaultCharset);
secondCall.setName(ast.newSimpleName(METHOD_DISPLAY_NAME));
return secondCall;
}
private static QualifiedName copyQualifiedName(AST ast, QualifiedName qualifiedName) {
return ast.newQualifiedName(ast.newName(qualifiedName.getQualifier().getFullyQualifiedName()),
ast.newSimpleName(qualifiedName.getName().getIdentifier()));
}
/** Resolve expression-name obscuring for this occurrence, not for the cached field. */
private static String aggregateOwnerName(CompilationUnit root, TypeDeclaration owner, ASTNode visited) {
ITypeBinding type= owner.resolveBinding();
if (type == null || type.isRecovered()) {
return null;
}
IBinding[] scope= new ScopeAnalyzer(root).getDeclarationsInScope(visited.getStartPosition(),
ScopeAnalyzer.VARIABLES | ScopeAnalyzer.TYPES | ScopeAnalyzer.CHECK_VISIBILITY);
String simple= owner.getName().getIdentifier();
boolean obscured= hasEnclosingTypeParameter(visited, simple);
for (IBinding binding : scope) {
if (simple.equals(binding.getName()) && !binding.isEqualTo(type)) {
obscured= true;
break;
}
}
if (!obscured) {
return simple;
}
String qualified= type.getTypeDeclaration().getQualifiedName();
int dot= qualified.indexOf('.');
if (dot < 0) {
return null; // A shadowed default-package type has no qualified alternative.
}
String first= qualified.substring(0, dot);
if (hasEnclosingTypeParameter(visited, first)) {
return null;
}
for (IBinding binding : scope) {
if (first.equals(binding.getName())) {
return null; // Qualification must not turn a package name into a variable/type.
}
}
for (ITypeBinding member= type; member.getDeclaringClass() != null; member= member.getDeclaringClass()) {
for (ITypeBinding declaring= member.getDeclaringClass(); declaring != null; declaring= declaring.getSuperclass()) {
for (IVariableBinding field : declaring.getDeclaredFields()) {
if (member.getName().equals(field.getName())) {
return null; // An intermediate member name may also denote a field.
}
}
}
}
return qualified;
}
private static boolean hasEnclosingTypeParameter(ASTNode visited, String name) {
for (ASTNode current= visited; current != null; current= current.getParent()) {
List<?> parameters;
if (current instanceof MethodDeclaration method) {
parameters= method.typeParameters();
} else if (current instanceof TypeDeclaration type) {
parameters= type.typeParameters();
} else {
continue;
}
for (Object parameter : parameters) {
if (name.equals(((TypeParameter) parameter).getName().getIdentifier())) {
return true;
}
}
}
return false;
}
private static TypeDeclaration findAggregateCharsetOwner(CompilationUnitRewrite cuRewrite, ASTNode visited) {
TypeDeclaration owner= ASTNodes.getFirstAncestorOrNull(visited, TypeDeclaration.class);
if (owner != null) {
return owner;
}
if (!cuRewrite.getRoot().types().isEmpty() && cuRewrite.getRoot().types().get(0) instanceof TypeDeclaration type) {
return type;
}
return null;
}
private static String aggregateCharsetOwnerKey(CompilationUnit root, TypeDeclaration owner) {
ITypeBinding binding= owner.resolveBinding();
if (binding != null) {
ITypeBinding declaration= binding.getTypeDeclaration();
if (declaration != null && declaration.getKey() != null) {
return declaration.getKey();
}
}
List<String> segments= new ArrayList<>();
for (ASTNode current= owner; current != null; current= current.getParent()) {
if (current instanceof TypeDeclaration type) {
segments.add(0, type.getName().getIdentifier());
}
}
String packageName= root.getPackage() == null ? "" : root.getPackage().getName().getFullyQualifiedName(); //$NON-NLS-1$
String localName= String.join(".", segments); //$NON-NLS-1$
return packageName.isEmpty() ? localName : packageName + '.' + localName;
}
private static String normalizeCharsetFieldName(String charset) {
return charset.toUpperCase(Locale.ROOT).replace('-', '_');
}
private static VariableDeclarationFragment findCompatibleCharsetField(TypeDeclaration owner, String requestedCharset) {
VariableDeclarationFragment fallback= null;
for (FieldDeclaration field : owner.getFields()) {
for (Object fragment : field.fragments()) {
if (fragment instanceof VariableDeclarationFragment variable
&& isCompatibleCharsetField(field, variable, requestedCharset)) {
if (requestedCharset.equals(variable.getName().getIdentifier())) {
return variable;
}
if (fallback == null) {
fallback= variable;
}
}
}
}
return fallback;
}
private static boolean isCompatibleCharsetField(FieldDeclaration field, VariableDeclarationFragment fragment,
String requestedCharset) {
if (!Modifier.isStatic(field.getModifiers()) || !Modifier.isFinal(field.getModifiers())
|| fragment.getInitializer() == null) {
return false;
}
ITypeBinding binding= field.getType().resolveBinding();
if (binding != null) {
if (!Charset.class.getCanonicalName().equals(binding.getErasure().getQualifiedName())) {
return false;
}
} else {
String typeName= field.getType().toString();
if (!Charset.class.getSimpleName().equals(typeName) && !typeName.endsWith(".Charset")) { //$NON-NLS-1$
return false;
}
}
String resolvedCharset= resolveCharsetInitializer(fragment.getInitializer());
return resolvedCharset != null && requestedCharset.equals(normalizeCharsetFieldName(resolvedCharset));
}
private static String resolveCharsetInitializer(Expression initializer) {
Expression expression= ASTNodes.getUnparenthesedExpression(initializer);
IVariableBinding field= null;
if (expression instanceof Name name && name.resolveBinding() instanceof IVariableBinding variable) {
field= variable;
} else if (expression instanceof FieldAccess fieldAccess) {
field= fieldAccess.resolveFieldBinding();
}
if (field != null && !field.isRecovered()) {
ITypeBinding declaringClass= field.getDeclaringClass();
if (declaringClass != null && !declaringClass.isRecovered()
&& StandardCharsets.class.getCanonicalName().equals(declaringClass.getErasure().getQualifiedName())
&& Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) {
return field.getName().replace('_', '-');
}
return null;
}
if (expression instanceof MethodInvocation methodInvocation
&& ASTNodes.usesGivenSignature(methodInvocation, Charset.class.getCanonicalName(), "forName", //$NON-NLS-1$
String.class.getCanonicalName())
&& methodInvocation.arguments().size() == 1
&& methodInvocation.arguments().get(0) instanceof StringLiteral literal) {
return literal.getLiteralValue().toUpperCase(Locale.ROOT);
}
return null;
}
private static boolean isSafeToReadField(TypeDeclaration owner, VariableDeclarationFragment targetField, ASTNode visited) {
if (!(targetField.getParent() instanceof FieldDeclaration targetDeclaration)
|| targetDeclaration.getParent() != owner || !Modifier.isStatic(targetDeclaration.getModifiers())) {
return false;
}
BodyDeclaration enclosingDeclaration= findEnclosingBodyDeclaration(owner, visited);
if (enclosingDeclaration == null) {
return false;
}
if (enclosingDeclaration instanceof FieldDeclaration fieldDeclaration
&& Modifier.isStatic(fieldDeclaration.getModifiers())) {
return isSafeFromStaticFieldInitializer(owner, fieldDeclaration, targetField, visited);
}
if (enclosingDeclaration instanceof Initializer initializer && Modifier.isStatic(initializer.getModifiers())) {
return bodyDeclarationIndex(owner, initializer) > bodyDeclarationIndex(owner, targetDeclaration);
}
// Methods (including constructors), instance initializers and nested types may
// be reached through callbacks while this class is still being initialized.
// A same-owner static-call graph cannot establish that those reads are safe.
return canInitializeOwnFieldsFirst(owner) && isFirstExecutingStaticInitializer(owner, targetField);
}
/**
* Even a newly inserted first field runs after superclass and default-method
* interface initialization. Without a proof about that hierarchy, keep direct
* typed JDK constants instead of introducing an initialization dependency.
* Local and non-static member classes also cannot host this field on older
* supported source levels, and an interface cannot contain a private field.
*/
private static boolean canInitializeOwnFieldsFirst(TypeDeclaration owner) {
ITypeBinding binding= owner.resolveBinding();
if (binding == null || binding.isRecovered() || owner.isInterface() || binding.isLocal()
|| binding.isAnonymous() || binding.isMember() && !Modifier.isStatic(binding.getModifiers())
|| binding.getInterfaces().length != 0) {
return false;
}
ITypeBinding superclass= binding.getSuperclass();
return superclass != null && !superclass.isRecovered()
&& Object.class.getCanonicalName().equals(superclass.getErasure().getQualifiedName());
}
private static boolean isFirstExecutingStaticInitializer(TypeDeclaration owner,
VariableDeclarationFragment targetField) {
for (Object declaration : owner.bodyDeclarations()) {
if (declaration instanceof Initializer initializer && Modifier.isStatic(initializer.getModifiers())) {
return false;
}
if (declaration instanceof FieldDeclaration field && Modifier.isStatic(field.getModifiers())) {
for (Object candidate : field.fragments()) {
if (candidate == targetField) {
return true;
}
if (candidate instanceof VariableDeclarationFragment fragment
&& fragment.getInitializer() != null
&& fragment.getInitializer().resolveConstantExpressionValue() == null) {
return false;
}
}
}
}
return false;
}
private static boolean isSafeFromStaticFieldInitializer(TypeDeclaration owner, FieldDeclaration enclosingDeclaration,
VariableDeclarationFragment targetField, ASTNode visited) {
FieldDeclaration targetDeclaration= (FieldDeclaration) targetField.getParent();
if (enclosingDeclaration != targetDeclaration) {
return bodyDeclarationIndex(owner, enclosingDeclaration) > bodyDeclarationIndex(owner, targetDeclaration);
}
VariableDeclarationFragment enclosingFragment= findEnclosingFragment(enclosingDeclaration, visited);
if (enclosingFragment == null) {
return false;
}
return fragmentIndex(enclosingDeclaration, enclosingFragment) > fragmentIndex(enclosingDeclaration, targetField);
}
private static BodyDeclaration findEnclosingBodyDeclaration(TypeDeclaration owner, ASTNode visited) {
for (ASTNode current= visited; current != null && current != owner; current= current.getParent()) {
if (current instanceof BodyDeclaration declaration && current.getParent() == owner) {
return declaration;
}
}
return null;
}
private static VariableDeclarationFragment findEnclosingFragment(FieldDeclaration declaration, ASTNode visited) {
for (ASTNode current= visited; current != null && current != declaration; current= current.getParent()) {
if (current instanceof VariableDeclarationFragment fragment && current.getParent() == declaration) {
return fragment;
}
}
return null;
}
private static int bodyDeclarationIndex(TypeDeclaration owner, BodyDeclaration declaration) {
return owner.bodyDeclarations().indexOf(declaration);
}
private static int fragmentIndex(FieldDeclaration declaration, VariableDeclarationFragment fragment) {
return declaration.fragments().indexOf(fragment);
}
private static String generateCharsetFieldName(TypeDeclaration owner, String ownerKey, String requestedFieldName,
Map<String, QualifiedName> charsetConstants) {
if (!hasFieldName(owner, requestedFieldName) && !hasReservedFieldName(ownerKey, requestedFieldName, charsetConstants)) {
return requestedFieldName;
}
for (int suffix= 1; ; suffix++) {
String candidate= requestedFieldName + '_' + suffix;
if (!hasFieldName(owner, candidate) && !hasReservedFieldName(ownerKey, candidate, charsetConstants)) {
return candidate;
}
}
}
private static boolean hasFieldName(TypeDeclaration owner, String fieldName) {
for (FieldDeclaration field : owner.getFields()) {
for (Object fragment : field.fragments()) {
if (fragment instanceof VariableDeclarationFragment variable
&& fieldName.equals(variable.getName().getIdentifier())) {
return true;
}
}
}
return false;
}
private static boolean hasReservedFieldName(String ownerKey, String fieldName,
Map<String, QualifiedName> charsetConstants) {
String prefix= ownerKey + '\u0000';
return charsetConstants.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(prefix))
.map(Map.Entry::getValue)
.anyMatch(name -> fieldName.equals(name.getName().getIdentifier()));
}
}