/*
* Copyright (c) 2013, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* 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 com.google.dart.java2dart.processor;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.dart.engine.ast.AssignmentExpression;
import com.google.dart.engine.ast.Block;
import com.google.dart.engine.ast.BlockFunctionBody;
import com.google.dart.engine.ast.ClassDeclaration;
import com.google.dart.engine.ast.ClassMember;
import com.google.dart.engine.ast.CompilationUnit;
import com.google.dart.engine.ast.ConstructorDeclaration;
import com.google.dart.engine.ast.EmptyFunctionBody;
import com.google.dart.engine.ast.Expression;
import com.google.dart.engine.ast.ExpressionFunctionBody;
import com.google.dart.engine.ast.ExpressionStatement;
import com.google.dart.engine.ast.FieldDeclaration;
import com.google.dart.engine.ast.FormalParameter;
import com.google.dart.engine.ast.MethodDeclaration;
import com.google.dart.engine.ast.MethodInvocation;
import com.google.dart.engine.ast.NodeList;
import com.google.dart.engine.ast.PropertyAccess;
import com.google.dart.engine.ast.RedirectingConstructorInvocation;
import com.google.dart.engine.ast.SimpleFormalParameter;
import com.google.dart.engine.ast.SimpleIdentifier;
import com.google.dart.engine.ast.Statement;
import com.google.dart.engine.ast.SuperConstructorInvocation;
import com.google.dart.engine.ast.ThisExpression;
import com.google.dart.engine.ast.TypeName;
import com.google.dart.engine.ast.VariableDeclaration;
import com.google.dart.engine.ast.VariableDeclarationList;
import com.google.dart.engine.ast.visitor.RecursiveAstVisitor;
import com.google.dart.engine.scanner.Keyword;
import com.google.dart.engine.scanner.TokenType;
import com.google.dart.java2dart.Context;
import com.google.dart.java2dart.util.Bindings;
import static com.google.dart.java2dart.util.AstFactory.assignmentExpression;
import static com.google.dart.java2dart.util.AstFactory.emptyFunctionBody;
import static com.google.dart.java2dart.util.AstFactory.fieldFormalParameter;
import static com.google.dart.java2dart.util.AstFactory.propertyAccess;
import static com.google.dart.java2dart.util.TokenFactory.token;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* {@link SemanticProcessor} for converting <code>getX()</code> or <code>setX(x)</code> into getter
* or setter.
*/
public class PropertySemanticProcessor extends SemanticProcessor {
private static class FieldPropertyInfo {
final ClassDeclaration clazz;
final String name;
MethodDeclaration getter;
MethodDeclaration setter;
VariableDeclaration getterField;
VariableDeclaration setterField;
VariableDeclaration field;
SimpleIdentifier fieldName;
String fieldFormalParameterName;
List<SimpleIdentifier> fieldAssignmentReferences;
public FieldPropertyInfo(ClassDeclaration clazz, String name) {
this.clazz = clazz;
this.name = name;
}
}
public static final String KEY_ORIGINAL_PARAMETER = "original-formal-parameter";
private static int getNumConstructorsWithBody(ClassDeclaration classDeclaration) {
int numConstructors = 0;
for (ClassMember member : classDeclaration.getMembers()) {
if (member instanceof ConstructorDeclaration) {
ConstructorDeclaration constructor = (ConstructorDeclaration) member;
if (constructor.getBody() instanceof EmptyFunctionBody) {
continue;
}
numConstructors++;
}
}
return numConstructors;
}
private static boolean hasPrefix(String name, String prefix) {
// should start with prefix
if (!name.startsWith(prefix)) {
return false;
}
// there should be one more character
int prefixLen = prefix.length();
if (name.length() < prefixLen + 1) {
return false;
}
// next character should be upper case (i.e. property name)
char nextChar = name.charAt(prefixLen);
return Character.isUpperCase(nextChar);
}
private static boolean isValidFieldProperty(FieldPropertyInfo property) {
// we need getter
VariableDeclaration getField = property.getterField;
if (property.getter == null || getField == null) {
return false;
}
// setter should be valid
VariableDeclaration setField = property.setterField;
if (property.setter != null && setField == null) {
return false;
}
// if there are both getter and setter, their fields should be the same
if (getField != null && setField != null && getField != setField) {
return false;
}
property.field = getField;
property.fieldName = property.field.getName();
// OK
return true;
}
private static boolean isValidSetterType(TypeName type) {
return type.getName().getName().equals("void");
}
/**
* Checks if {@link CompilationUnit} declares field with name "onlyBasicGettersSetters", removes
* it and return {@code true}.
*/
private static boolean onlyBasicGettersSetters(CompilationUnit unit) {
final AtomicBoolean result = new AtomicBoolean();
unit.accept(new RecursiveAstVisitor<Void>() {
@Override
public Void visitVariableDeclaration(VariableDeclaration node) {
if (node.getName().getName().equals("_onlyBasicGettersSetters")) {
FieldDeclaration fieldDeclaration = (FieldDeclaration) node.getParent().getParent();
ClassDeclaration clazz = (ClassDeclaration) fieldDeclaration.getParent();
clazz.getMembers().remove(fieldDeclaration);
result.set(true);
}
return null;
}
});
return result.get();
}
/**
* Removes the given {@link ClassMember} from its parent {@link ClassDeclaration}.
*/
private static void removeClassMember(ClassMember member) {
ClassDeclaration clazz = (ClassDeclaration) member.getParent();
List<ClassMember> members = clazz.getMembers();
members.remove(member);
}
public PropertySemanticProcessor(Context context) {
super(context);
}
public void convertToField(FieldPropertyInfo property) {
MethodDeclaration getter = property.getter;
MethodDeclaration setter = property.setter;
VariableDeclaration field = property.field;
SimpleIdentifier fieldName = property.fieldName;
int numConstructors = getNumConstructorsWithBody(property.clazz);
// analyze field assignments
boolean canBeFinal;
{
getAssignmentReferencesInConstructors(property);
// analyze constructor assignments
List<SimpleIdentifier> references = property.fieldAssignmentReferences;
canBeFinal = references != null && references.size() == numConstructors;
// if there are no setter and we cannot make the field final, then keep getter/setter
if (setter == null && !canBeFinal) {
return;
}
// convert to "this.property" in constructors
if (references != null) {
convertToFieldFormalInitializers(property, references);
}
}
// update field
{
context.renameIdentifier(fieldName, property.name);
// mark "final" if no writes
boolean readOnly = isReadOnlyField(fieldName);
if (readOnly) {
((VariableDeclarationList) field.getParent()).setKeyword(token(Keyword.FINAL));
if (canBeFinal && numConstructors != 0) {
field.setInitializer(null);
}
}
}
// remove getter, update field
if (getter != null) {
removeClassMember(getter);
}
// remove setter
if (setter != null) {
removeClassMember(setter);
}
// now these are field references
{
IBinding fieldBinding = context.getNodeBinding(fieldName);
replaceMethodReferencesWithFieldBindings(getter, fieldBinding);
replaceMethodReferencesWithFieldBindings(setter, fieldBinding);
}
}
public void convertToFields(CompilationUnit unit) {
final Set<IBinding> overriddenMethods = getOverriddenMethods(unit);
unit.accept(new RecursiveAstVisitor<Void>() {
@Override
public Void visitClassDeclaration(ClassDeclaration node) {
List<FieldPropertyInfo> properties = getFieldProperties(node);
removeOverriddenProperties(overriddenMethods, properties);
for (FieldPropertyInfo property : properties) {
if (!isValidFieldProperty(property)) {
continue;
}
convertToField(property);
}
return null;
}
});
}
public void convertToFinalFields(CompilationUnit unit) {
unit.accept(new RecursiveAstVisitor<Void>() {
@Override
public Void visitClassDeclaration(ClassDeclaration node) {
NodeList<ClassMember> members = node.getMembers();
for (ClassMember member : members) {
if (member instanceof FieldDeclaration) {
FieldDeclaration fieldDeclaration = (FieldDeclaration) member;
VariableDeclaration field = fieldDeclaration.getFields().getVariables().get(0);
processField(node, field);
}
}
return null;
}
private void processField(ClassDeclaration node, VariableDeclaration field) {
SimpleIdentifier fieldNameNode = field.getName();
String fieldName = fieldNameNode.getName();
if (!fieldName.startsWith("_")) {
return;
}
String propertyName = fieldName.substring(1);
// prepare property
FieldPropertyInfo property = new FieldPropertyInfo(node, propertyName);
property.fieldName = fieldNameNode;
property.fieldFormalParameterName = fieldName;
int numConstructors = getNumConstructorsWithBody(property.clazz);
// analyze field assignments
getAssignmentReferencesInConstructors(property);
List<SimpleIdentifier> references = property.fieldAssignmentReferences;
if (references == null || numConstructors == 0 || references.size() != numConstructors) {
return;
}
// convert to "this.property" in constructors
convertToFieldFormalInitializers(property, references);
// update field
((VariableDeclarationList) field.getParent()).setKeyword(token(Keyword.FINAL));
field.setInitializer(null);
}
});
}
@Override
public void process(CompilationUnit unit) {
convertGettersSetters(unit);
if (onlyBasicGettersSetters(unit)) {
return;
}
convertToFields(unit);
convertToFinalFields(unit);
}
/**
* Converts every <code>getX()</code> into <code>get x</code> and <code>setX(v)</code> into
* <code>set x(v)</code> with corresponding replacement of {@link MethodInvocation} into
* {@link PropertyAccess} in references.
*/
private void convertGettersSetters(CompilationUnit unit) {
unit.accept(new RecursiveAstVisitor<Void>() {
@Override
public Void visitMethodDeclaration(MethodDeclaration node) {
if (node.getName() instanceof SimpleIdentifier && node.getParameters() != null) {
SimpleIdentifier nameNode = node.getName();
String name = context.getIdentifierOriginalName(nameNode);
List<FormalParameter> parameters = node.getParameters().getParameters();
// getter
if (parameters.isEmpty()
&& (hasPrefix(name, "get") || hasPrefix(name, "is") || hasPrefix(name, "has"))) {
if (!context.canMakeProperty(nameNode)) {
return null;
}
String propertyName = name;
if (hasPrefix(name, "get")) {
propertyName = StringUtils.removeStart(name, "get");
} else if (hasPrefix(name, "is")) {
// don't remove "is" prefix, for example because we want to keep "isSynthetic" name
}
propertyName = StringUtils.uncapitalize(propertyName);
// rename references
context.renameIdentifier(nameNode, propertyName);
// replace MethodInvocation with PropertyAccess
for (SimpleIdentifier reference : context.getReferences(nameNode)) {
if (reference.getParent() instanceof MethodInvocation) {
MethodInvocation invocation = (MethodInvocation) reference.getParent();
if (invocation.getMethodName() == reference) {
Expression invocationTarget = invocation.getTarget();
// prepare replacement
Expression replacement;
if (invocationTarget != null) {
replacement = propertyAccess(invocationTarget, reference);
} else {
replacement = reference;
}
// do replace
replaceNode(invocation, replacement);
}
}
}
// convert method to getter
node.setPropertyKeyword(token(Keyword.GET));
node.setParameters(null);
}
// setter
if (hasPrefix(name, "set") && parameters.size() == 1
&& isValidSetterType(node.getReturnType())) {
if (!context.canMakeProperty(nameNode)) {
return null;
}
String propertyName = StringUtils.uncapitalize(StringUtils.removeStart(name, "set"));
// rename references
context.renameIdentifier(nameNode, propertyName);
// replace MethodInvocation with AssignmentExpression
for (SimpleIdentifier reference : context.getReferences(nameNode)) {
if (reference.getParent() instanceof MethodInvocation) {
MethodInvocation invocation = (MethodInvocation) reference.getParent();
if (invocation.getMethodName() == reference) {
Expression invocationTarget = invocation.getTarget();
List<Expression> arguments = invocation.getArgumentList().getArguments();
// prepare assignment target
Expression assignmentTarget;
if (invocationTarget != null) {
assignmentTarget = propertyAccess(invocationTarget, reference);
} else {
assignmentTarget = reference;
}
// do replace
replaceNode(
invocation,
assignmentExpression(assignmentTarget, TokenType.EQ, arguments.get(0)));
}
}
}
// convert method to setter
node.setPropertyKeyword(token(Keyword.SET));
}
}
return super.visitMethodDeclaration(node);
}
});
}
private void convertToFieldFormalInitializers(FieldPropertyInfo property,
List<SimpleIdentifier> references) {
for (SimpleIdentifier reference : references) {
// prepare constructor
ConstructorDeclaration constructor = reference.getAncestor(ConstructorDeclaration.class);
// convert "formal parameter" into "field initializing formal parameter"
List<FormalParameter> parameters = constructor.getParameters().getParameters();
for (FormalParameter parameter : parameters) {
if (parameter instanceof SimpleFormalParameter) {
SimpleFormalParameter simpleParameter = (SimpleFormalParameter) parameter;
SimpleIdentifier parameterName = simpleParameter.getIdentifier();
if (parameterName.getName().equals(property.name)) {
if (property.fieldFormalParameterName != null) {
context.renameIdentifier(parameterName, property.fieldFormalParameterName);
}
FormalParameter ffp = fieldFormalParameter(null, null, parameterName);
ffp.setProperty(KEY_ORIGINAL_PARAMETER, parameter);
replaceNode(simpleParameter, ffp);
break;
}
}
}
// remove assignment statement
Statement statement = (Statement) reference.getParent().getParent().getParent();
List<Statement> statements = removeBlockStatement(statement);
if (statements.isEmpty()) {
constructor.setBody(emptyFunctionBody());
}
}
}
/**
* If the given {@link SimpleIdentifier} is the field reference and:
* <ul>
* <li>The field is assigned only in constructors.</li>
* <li>The field is assigned only once in each constructor.</li>
* <li>The assignment looks like <code>this.name = name;</code> statement.</li>
* </ul>
* Then returns all references of the field in these simple assignments. Otherwise returns
* {@code null}.
*/
private void getAssignmentReferencesInConstructors(FieldPropertyInfo property) {
Set<ConstructorDeclaration> constructorsWithAssignments = Sets.newHashSet();
List<SimpleIdentifier> assignReferences = Lists.newArrayList();
List<SimpleIdentifier> references = context.getReferences(property.fieldName);
for (SimpleIdentifier reference : references) {
// not in setter context
if (!reference.inSetterContext()) {
continue;
}
// should be in constructor
ConstructorDeclaration constructor = reference.getAncestor(ConstructorDeclaration.class);
if (constructor == null) {
if (reference.getAncestor(MethodDeclaration.class) == property.setter) {
continue;
}
return;
}
// may be more than one assignment
if (constructorsWithAssignments.contains(constructor)) {
return;
}
constructorsWithAssignments.add(constructor);
// should be "this.name"
if (!(reference.getParent() instanceof PropertyAccess)) {
return;
}
PropertyAccess propertyAccess = (PropertyAccess) reference.getParent();
if (!(propertyAccess.getTarget() instanceof ThisExpression)) {
return;
}
// should be assignment
if (!(propertyAccess.getParent() instanceof AssignmentExpression)) {
return;
}
AssignmentExpression assignment = (AssignmentExpression) propertyAccess.getParent();
// should be "this.name = _name"
if (assignment.getRightHandSide() instanceof SimpleIdentifier) {
SimpleIdentifier right = (SimpleIdentifier) assignment.getRightHandSide();
if (isReferencedInSuperConstructorInvocation(right)) {
continue;
}
String rightName = right.getName();
String leftName = reference.getName();
if (!isFieldWithPropertyName(leftName, rightName)) {
return;
}
} else {
return;
}
// should be simple, just in statement
if (!(assignment.getParent() instanceof ExpressionStatement)) {
return;
}
// should be just "="
if (assignment.getOperator().getType() != TokenType.EQ) {
return;
}
// OK, valid assignment reference
assignReferences.add(reference);
}
// OK, no other assignments found
property.fieldAssignmentReferences = assignReferences;
}
private List<FieldPropertyInfo> getFieldProperties(final ClassDeclaration classDeclaration) {
final Map<String, FieldPropertyInfo> properties = Maps.newHashMap();
classDeclaration.accept(new RecursiveAstVisitor<Void>() {
@Override
public Void visitMethodDeclaration(MethodDeclaration node) {
// we need getter or setter
if (!node.isGetter() && !node.isSetter()) {
return null;
}
// prepare property
String propertyName = node.getName().getName();
FieldPropertyInfo property = getProperty(propertyName);
// getter
if (node.isGetter()) {
processGetter(property, node);
}
// setter
if (node.isSetter()) {
processSetter(property, node);
}
// done
return null;
}
private FieldPropertyInfo getProperty(String name) {
FieldPropertyInfo property = properties.get(name);
if (property == null) {
property = new FieldPropertyInfo(classDeclaration, name);
properties.put(name, property);
}
return property;
}
private void processGetter(FieldPropertyInfo property, MethodDeclaration node) {
property.getter = node;
if (node.getBody() instanceof ExpressionFunctionBody) {
ExpressionFunctionBody body = (ExpressionFunctionBody) node.getBody();
Expression expression = body.getExpression();
if (expression instanceof SimpleIdentifier) {
SimpleIdentifier identifier = (SimpleIdentifier) expression;
String fieldName = identifier.getName();
// may be not a local field
VariableDeclaration field = classDeclaration.getField(fieldName);
if (field == null) {
return;
}
// maybe field doesn't have the same name as the property
if (!isFieldWithPropertyName(fieldName, property.name)) {
return;
}
// OK, remember the field
property.getterField = field;
}
}
}
private void processSetter(FieldPropertyInfo property, MethodDeclaration node) {
property.setter = node;
// block body
if (!(node.getBody() instanceof BlockFunctionBody)) {
return;
}
// single statement
BlockFunctionBody body = (BlockFunctionBody) node.getBody();
List<Statement> statements = body.getBlock().getStatements();
if (statements.size() != 1) {
return;
}
Statement statement = statements.get(0);
// prepare expression
if (!(statement instanceof ExpressionStatement)) {
return;
}
Expression expression = ((ExpressionStatement) statement).getExpression();
// should be assignment
if (!(expression instanceof AssignmentExpression)) {
return;
}
AssignmentExpression assignment = (AssignmentExpression) expression;
// simple assignment
if (assignment.getOperator().getType() != TokenType.EQ) {
return;
}
// RHS should be just parameter name
Expression rhs = assignment.getRightHandSide();
if (!(rhs instanceof SimpleIdentifier)) {
return;
}
SimpleIdentifier rhsName = (SimpleIdentifier) rhs;
List<FormalParameter> parameters = node.getParameters().getParameters();
String parameterName = ((SimpleFormalParameter) parameters.get(0)).getIdentifier().getName();
if (!rhsName.getName().equals(parameterName)) {
return;
}
// LHS
Expression lhs = assignment.getLeftHandSide();
if (lhs instanceof PropertyAccess) {
PropertyAccess access = (PropertyAccess) lhs;
if (access.getTarget() instanceof ThisExpression) {
String fieldName = access.getPropertyName().getName();
// may be not a local field
VariableDeclaration field = classDeclaration.getField(fieldName);
if (field == null) {
return;
}
// may be field has not the same name as the property
if (!isFieldWithPropertyName(fieldName, property.name)) {
return;
}
// OK, remember the field
property.setterField = field;
}
}
}
});
return Lists.newArrayList(properties.values());
}
private Set<IBinding> getOverriddenMethods(CompilationUnit unit) {
final Set<IBinding> overriddenMethods = Sets.newHashSet();
unit.accept(new RecursiveAstVisitor<Void>() {
@Override
public Void visitMethodDeclaration(MethodDeclaration node) {
IMethodBinding binding = (IMethodBinding) context.getNodeBinding(node);
if (binding != null) {
IMethodBinding superBinding = Bindings.findOverriddenMethod(binding, true);
if (superBinding != null) {
overriddenMethods.add(superBinding);
}
}
return null;
}
});
return overriddenMethods;
}
private boolean isFieldWithPropertyName(String fieldName, String propertyName) {
String prefix;
if (fieldName.startsWith("_" + propertyName)) {
// field was private
prefix = "_" + propertyName;
} else if (fieldName.startsWith(propertyName)) {
// field was protected
prefix = propertyName;
} else {
return false;
}
String rest = fieldName.substring(prefix.length());
if (!StringUtils.isNumericSpace(rest)) {
return false;
}
return true;
}
private boolean isOverridden(Set<IBinding> overriddenMethods, MethodDeclaration method) {
if (method == null) {
return false;
}
IBinding binding = context.getNodeBinding(method);
return overriddenMethods.contains(binding);
}
private boolean isReadOnlyField(SimpleIdentifier fieldName) {
List<SimpleIdentifier> references = context.getReferences(fieldName);
for (SimpleIdentifier reference : references) {
if (reference.inSetterContext()) {
return false;
}
}
return true;
}
/**
* @return {@code true} if the given parameter is referenced
* {@link RedirectingConstructorInvocation} or {@link SuperConstructorInvocation}.
*/
private boolean isReferencedInSuperConstructorInvocation(SimpleIdentifier parameter) {
List<SimpleIdentifier> references = context.getReferences(parameter);
for (SimpleIdentifier reference : references) {
if (reference.getAncestor(SuperConstructorInvocation.class) != null) {
return true;
}
}
return false;
}
/**
* Removes given {@link Statement} from its {@link Block}.
*/
private NodeList<Statement> removeBlockStatement(Statement statement) {
NodeList<Statement> statements = ((Block) statement.getParent()).getStatements();
statements.remove(statement);
context.clearNodes(statement);
return statements;
}
private void removeOverriddenProperties(Set<IBinding> overriddenMethods,
List<FieldPropertyInfo> properties) {
for (Iterator<FieldPropertyInfo> I = properties.iterator(); I.hasNext();) {
FieldPropertyInfo property = I.next();
if (isOverridden(overriddenMethods, property.getter)
|| isOverridden(overriddenMethods, property.setter)) {
I.remove();
}
}
}
private void replaceMethodReferencesWithFieldBindings(MethodDeclaration method,
IBinding fieldBinding) {
if (method == null) {
return;
}
SimpleIdentifier name = method.getName();
List<SimpleIdentifier> references = context.getReferences(name);
references = ImmutableList.copyOf(references);
for (SimpleIdentifier reference : references) {
context.putReference(reference, fieldBinding, null);
}
}
}