blob: 80c8fd41579fdb0e1b24bb96c8946210434d19b7 [file] [log] [blame]
/*
* Copyright 2015 Google Inc.
*
* 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
*
* http://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 com.google.gwt.dev.jjs.impl;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.MinimalRebuildCache;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.JConstructor;
import com.google.gwt.dev.jjs.ast.JDeclaredType;
import com.google.gwt.dev.jjs.ast.JExpressionStatement;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JInterfaceType;
import com.google.gwt.dev.jjs.ast.JMember;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethod.JsPropertyType;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JPrimitiveType;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JStatement;
import com.google.gwt.dev.jjs.ast.JType;
import com.google.gwt.dev.jjs.ast.JVisitor;
import com.google.gwt.thirdparty.guava.common.base.Predicate;
import com.google.gwt.thirdparty.guava.common.collect.FluentIterable;
import com.google.gwt.thirdparty.guava.common.collect.Iterables;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Checks and throws errors for invalid JsInterop constructs.
*/
// TODO: handle custom JsType field/method names when that feature exists.
// TODO: move JsInterop checks from JSORestrictionsChecker to here.
// TODO: provide more information in global name collisions as it could be difficult to pinpoint in
// big projects.
public class JsInteropRestrictionChecker extends JVisitor {
public static void exec(TreeLogger logger, JProgram jprogram,
MinimalRebuildCache minimalRebuildCache) throws UnableToCompleteException {
JsInteropRestrictionChecker jsInteropRestrictionChecker =
new JsInteropRestrictionChecker(logger, jprogram, minimalRebuildCache);
jsInteropRestrictionChecker.accept(jprogram);
if (jsInteropRestrictionChecker.hasErrors) {
throw new UnableToCompleteException();
}
}
private Map<String, String> currentJsTypeMethodNameByGetterNames;
private Map<String, String> currentJsTypeMethodNameByMemberNames;
private Map<String, String> currentJsTypeMethodNameBySetterNames;
private Set<JMethod> currentJsTypeProcessedMethods;
private Map<String, JType> currentJsTypePropertyTypeByName;
private JDeclaredType currentType;
private boolean hasErrors;
private final JProgram jprogram;
private final TreeLogger logger;
private final MinimalRebuildCache minimalRebuildCache;
public JsInteropRestrictionChecker(TreeLogger logger, JProgram jprogram,
MinimalRebuildCache minimalRebuildCache) {
this.logger = logger;
this.jprogram = jprogram;
this.minimalRebuildCache = minimalRebuildCache;
}
@Override
public void endVisit(JDeclaredType x, Context ctx) {
assert currentType == x;
currentType = null;
}
@Override
public boolean visit(JDeclaredType x, Context ctx) {
assert currentType == null;
currentJsTypeProcessedMethods = Sets.newHashSet();
currentJsTypePropertyTypeByName = Maps.newHashMap();
currentJsTypeMethodNameByMemberNames = Maps.newHashMap();
currentJsTypeMethodNameByGetterNames = Maps.newHashMap();
currentJsTypeMethodNameBySetterNames = Maps.newHashMap();
minimalRebuildCache.removeJsInteropNames(x.getName());
currentType = x;
checkJsFunctionHierarchy(x);
if (currentType instanceof JInterfaceType) {
checkJsTypeHierarchy((JInterfaceType) currentType);
} else {
checkConstructors(x);
}
// Perform custom class traversal to examine fields and methods of this class and all
// superclasses so that name collisions between local and inherited members can be found.
do {
acceptWithInsertRemoveImmutable(x.getFields());
acceptWithInsertRemoveImmutable(x.getMethods());
x = x.getSuperClass();
} while (x != null);
// Skip the default class traversal.
return false;
}
private void checkConstructors(JDeclaredType x) {
List<JMethod> exportedCtors = FluentIterable
.from(x.getMethods())
.filter(new Predicate<JMethod>() {
@Override
public boolean apply(JMethod m) {
return m.isExported() && m instanceof JConstructor;
}
}).toList();
if (exportedCtors.isEmpty()) {
return;
}
if (exportedCtors.size() > 1) {
logError("More than one constructor exported for %s.", x.getName());
}
final JConstructor exportedCtor = (JConstructor) exportedCtors.get(0);
if (!exportedCtor.getExportName().isEmpty()) {
logError("Constructor '%s' cannot have an export name.", exportedCtor.getQualifiedName());
}
boolean anyNonDelegatingConstructor = Iterables.any(x.getMethods(), new Predicate<JMethod>() {
@Override
public boolean apply(JMethod method) {
return method != exportedCtor && method instanceof JConstructor
&& !isDelegatingToConstructor((JConstructor) method, exportedCtor);
}
});
if (anyNonDelegatingConstructor) {
logError("Constructor '%s' can only be exported if all constructors in the class are "
+ "delegating to it.", exportedCtor.getQualifiedName());
}
}
private boolean isDelegatingToConstructor(JConstructor ctor, JConstructor targetCtor) {
List<JStatement> statements = ctor.getBody().getBlock().getStatements();
JExpressionStatement statement = (JExpressionStatement) statements.get(0);
JMethodCall call = (JMethodCall) statement.getExpr();
assert call.isStaticDispatchOnly() : "Every ctor should either have this() or super() call";
return call.getTarget().equals(targetCtor);
}
@Override
public boolean visit(JField x, Context ctx) {
if (currentType == x.getEnclosingType() && x.isExported()) {
checkExportName(x);
} else if (x.isJsTypeMember()) {
checkJsTypeFieldName(x, x.getJsMemberName());
}
return false;
}
@Override
public boolean visit(JMethod x, Context ctx) {
if (!currentJsTypeProcessedMethods.add(x)) {
return false;
}
currentJsTypeProcessedMethods.addAll(x.getOverriddenMethods());
if (currentType == x.getEnclosingType() && x.isExported()) {
checkExportName(x);
} else if (x.isOrOverridesJsTypeMethod()) {
checkJsTypeMethod(x);
}
if (currentType == x.getEnclosingType()) {
if (x.isJsPropertyAccessor() && !currentType.isJsType()) {
if (currentType instanceof JInterfaceType) {
logError("Method '%s' can't be a JsProperty since interface '%s' is not a JsType.",
x.getName(), x.getEnclosingType().getName());
} else {
logError("Method '%s' can't be a JsProperty since '%s' "
+ "is not an interface.", x.getName(), x.getEnclosingType().getName());
}
}
}
return false;
}
private void checkExportName(JMember x) {
boolean success = minimalRebuildCache.addExportedGlobalName(x.getQualifiedExportName(),
currentType.getName());
if (!success) {
logError("Member '%s' can't be exported because the global name '%s' is already taken.",
x.getQualifiedName(), x.getQualifiedExportName());
}
}
private void checkInconsistentPropertyType(String propertyName, String enclosingTypeName,
JType parameterType) {
JType recordedType = currentJsTypePropertyTypeByName.put(propertyName, parameterType);
if (recordedType != null && recordedType != parameterType) {
logError("The setter and getter for JsProperty '%s' in type '%s' must have consistent types.",
propertyName, enclosingTypeName);
}
}
private void checkJsTypeHierarchy(JInterfaceType interfaceType) {
if (currentType.isJsType()) {
for (JDeclaredType superInterface : interfaceType.getImplements()) {
if (!superInterface.isJsType()) {
logWarning(
"JsType interface '%s' extends non-JsType interface '%s'. This is not recommended.",
interfaceType.getName(), superInterface.getName());
}
}
}
}
private void checkJsTypeFieldName(JField field, String memberName) {
boolean success =
currentJsTypeMethodNameByMemberNames.put(memberName, field.getQualifiedName()) == null;
if (!success) {
logError("Field '%s' can't be exported in type '%s' because the member name "
+ "'%s' is already taken.", field.getQualifiedName(), currentType.getName(), memberName);
}
}
private void checkJsTypeMethod(JMethod method) {
if (method.isSynthetic() && !method.isForwarding()) {
// A name slot taken up by a synthetic method, such as a bridge method for a generic method,
// is not the fault of the user and so should not be reported as an error. JS generation
// should take responsibility for ensuring that only the correct method version (in this
// particular set of colliding method names) is exported. Forwarding synthetic methods
// (such as an accidental override forwarding method that occurs when a JsType interface
// starts exposing a method in class B that is only ever implemented in its parent class A)
// though should be checked since they are exported and do take up an name slot.
return;
}
String jsMemberName = method.getImmediateOrTransitiveJsMemberName();
String qualifiedMethodName = method.getQualifiedName();
String typeName = method.getEnclosingType().getName();
JsPropertyType jsPropertyType = method.getImmediateOrTransitiveJsPropertyType();
if (jsMemberName == null) {
logError("'%s' can't be exported because the method overloads multiple methods with "
+ "different names.", qualifiedMethodName);
}
if (jsPropertyType == JsPropertyType.GET) {
if (!method.getParams().isEmpty() || method.getType() == JPrimitiveType.VOID) {
logError("There can't be void return type or any parameters for the JsProperty getter"
+ " '%s'.", qualifiedMethodName);
return;
}
if (method.getType() != JPrimitiveType.BOOLEAN && method.getName().startsWith("is")) {
logError("There can't be non-booelean return for the JsProperty 'is' getter '%s'.",
qualifiedMethodName);
return;
}
if (currentJsTypeMethodNameByGetterNames.put(jsMemberName, qualifiedMethodName) != null) {
// Don't allow multiple getters for the same property name.
logError("There can't be more than one getter for JsProperty '%s' in type '%s'.",
jsMemberName, typeName);
return;
}
checkNameCollisionForGetterAndRegular(jsMemberName, typeName);
checkInconsistentPropertyType(jsMemberName, typeName, method.getOriginalReturnType());
} else if (jsPropertyType == JsPropertyType.SET) {
if (method.getParams().size() != 1 || method.getType() != JPrimitiveType.VOID) {
logError("There needs to be single parameter and void return type for the JsProperty setter"
+ " '%s'.", qualifiedMethodName);
return;
}
if (currentJsTypeMethodNameBySetterNames.put(jsMemberName, qualifiedMethodName) != null) {
// Don't allow multiple setters for the same property name.
logError("There can't be more than one setter for JsProperty '%s' in type '%s'.",
jsMemberName, typeName);
return;
}
checkNameCollisionForSetterAndRegular(jsMemberName, typeName);
checkInconsistentPropertyType(jsMemberName, typeName,
Iterables.getOnlyElement(method.getParams()).getType());
} else if (jsPropertyType == JsPropertyType.UNDEFINED) {
// We couldn't extract the JsPropertyType.
logError("JsProperty '%s' doesn't follow Java Bean naming conventions.", qualifiedMethodName);
} else {
// If it's just an regular JsType method.
if (currentJsTypeMethodNameByMemberNames.put(jsMemberName, qualifiedMethodName) != null) {
logError("Method '%s' can't be exported in type '%s' because the member name "
+ "'%s' is already taken.", qualifiedMethodName, currentType.getName(), jsMemberName);
}
checkNameCollisionForGetterAndRegular(jsMemberName, typeName);
checkNameCollisionForSetterAndRegular(jsMemberName, typeName);
}
}
private void checkNameCollisionForGetterAndRegular(String getterName, String typeName) {
if (currentJsTypeMethodNameByGetterNames.containsKey(getterName)
&& currentJsTypeMethodNameByMemberNames.containsKey(getterName)) {
logError("The JsType member '%s' and JsProperty '%s' can't both be named "
+ "'%s' in type '%s'.", currentJsTypeMethodNameByMemberNames.get(getterName),
currentJsTypeMethodNameByGetterNames.get(getterName), getterName, typeName);
}
}
private void checkNameCollisionForSetterAndRegular(String setterName, String typeName) {
if (currentJsTypeMethodNameBySetterNames.containsKey(setterName)
&& currentJsTypeMethodNameByMemberNames.containsKey(setterName)) {
logError("The JsType member '%s' and JsProperty '%s' can't both be named "
+ "'%s' in type '%s'.", currentJsTypeMethodNameByMemberNames.get(setterName),
currentJsTypeMethodNameBySetterNames.get(setterName), setterName, typeName);
}
}
private void checkJsFunctionHierarchy(JDeclaredType type) {
if (!type.isOrExtendsJsFunction()) {
return;
}
List<JInterfaceType> implementedInterfaces = type.getImplements();
if (type.isJsFunction()) {
if (implementedInterfaces.size() > 0) {
logError("JsFunction '%s' cannot extend other interfaces.", type);
}
if (type.isJsType()) {
logError("'%s' cannot be both a JsFunction and a JsType at the same time.", type);
}
return;
}
if (type instanceof JInterfaceType) {
logError("Interface '%s' cannot extend a JsFunction interface.", type);
return;
}
if (implementedInterfaces.size() != 1) {
logError("JsFunction implementation '%s' cannot implement more than one interface.", type);
}
if (type.isJsType()) {
logError("'%s' cannot be both a JsFunction implementation and a JsType at the same time.",
type);
}
if (type.getSuperClass() != jprogram.getTypeJavaLangObject()) {
logError("JsFunction implementation '%s' cannot extend a class.", type);
}
}
private void logError(String format, JType type) {
logError(format, type.getName());
}
private void logError(String format, Object... args) {
logger.log(TreeLogger.ERROR, String.format(format, args));
hasErrors = true;
}
private void logWarning(String format, Object... args) {
logger.log(TreeLogger.WARN, String.format(format, args));
}
}