blob: 23f52db12fe8ec50d829d09965d5391ea8673282 [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.javac.JsInteropUtil;
import com.google.gwt.dev.jjs.HasSourceInfo;
import com.google.gwt.dev.jjs.ast.CanBeJsNative;
import com.google.gwt.dev.jjs.ast.CanHaveSuppressedWarnings;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.HasJsInfo.JsMemberType;
import com.google.gwt.dev.jjs.ast.HasJsName;
import com.google.gwt.dev.jjs.ast.HasType;
import com.google.gwt.dev.jjs.ast.JClassType;
import com.google.gwt.dev.jjs.ast.JConstructor;
import com.google.gwt.dev.jjs.ast.JDeclaredType;
import com.google.gwt.dev.jjs.ast.JDeclaredType.NestedClassDisposition;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JExpressionStatement;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JInstanceOf;
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.JMethodBody;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JParameter;
import com.google.gwt.dev.jjs.ast.JPrimitiveType;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JReferenceType;
import com.google.gwt.dev.jjs.ast.JStatement;
import com.google.gwt.dev.jjs.ast.JVisitor;
import com.google.gwt.dev.jjs.ast.js.JsniMethodBody;
import com.google.gwt.dev.js.JsUtils;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsFunction;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsParameter;
import com.google.gwt.dev.js.ast.JsVisitor;
import com.google.gwt.dev.util.Pair;
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 java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Checks and throws errors for invalid JsInterop constructs.
*/
public class JsInteropRestrictionChecker extends AbstractRestrictionChecker {
public static void exec(TreeLogger logger, JProgram jprogram,
MinimalRebuildCache minimalRebuildCache) throws UnableToCompleteException {
JsInteropRestrictionChecker jsInteropRestrictionChecker =
new JsInteropRestrictionChecker(jprogram, minimalRebuildCache);
boolean success = jsInteropRestrictionChecker.checkProgram(logger);
if (!success) {
throw new UnableToCompleteException();
}
}
private final JProgram jprogram;
private final MinimalRebuildCache minimalRebuildCache;
private boolean wasUnusableByJsWarningReported = false;
private JsInteropRestrictionChecker(JProgram jprogram,
MinimalRebuildCache minimalRebuildCache) {
this.jprogram = jprogram;
this.minimalRebuildCache = minimalRebuildCache;
}
/**
* Returns true if the constructor method is locally empty (allows calls to init and super
* constructor).
*/
private static boolean isConstructorEmpty(final JConstructor constructor) {
return Iterables.all(constructor.getBody().getStatements(), new Predicate<JStatement>() {
@Override
public boolean apply(JStatement statement) {
JClassType type = constructor.getEnclosingType();
if (isImplicitSuperCall(statement, type.getSuperClass())) {
return true;
}
if (isInitCall(statement, type)) {
return true;
}
return false;
}
});
}
private static JMethodCall isMethodCall(JStatement statement) {
if (!(statement instanceof JExpressionStatement)) {
return null;
}
JExpression expression = ((JExpressionStatement) statement).getExpr();
return expression instanceof JMethodCall ? (JMethodCall) expression : null;
}
private static boolean isInitCall(JStatement statement, JDeclaredType type) {
JMethodCall methodCall = isMethodCall(statement);
return methodCall != null
&& methodCall.getTarget() == type.getInitMethod();
}
private static boolean isImplicitSuperCall(JStatement statement, JDeclaredType superType) {
JMethodCall methodCall = isMethodCall(statement);
return methodCall != null
&& methodCall.isStaticDispatchOnly()
&& methodCall.getTarget().isConstructor()
&& methodCall.getTarget().getEnclosingType() == superType;
}
private static boolean isInitEmpty(JDeclaredType type) {
return type.getInitMethod() == null
|| ((JMethodBody) type.getInitMethod().getBody()).getStatements().isEmpty();
}
private void checkJsConstructors(JDeclaredType type) {
List<JConstructor> jsConstructors = getJsConstructors(type);
if (type.isJsNative()) {
return;
}
if (jsConstructors.isEmpty()) {
return;
}
if (jsConstructors.size() > 1) {
logError(type,
"More than one JsConstructor exists for %s.", getDescription(type));
}
final JConstructor jsConstructor = jsConstructors.get(0);
if (JjsUtils.getPrimaryConstructor(type) != jsConstructor) {
logError(jsConstructor,
"Constructor %s can be a JsConstructor only if all constructors in the class are "
+ "delegating to it.", getMemberDescription(jsConstructor));
}
}
private List<JConstructor> getJsConstructors(JDeclaredType type) {
return FluentIterable
.from(type.getConstructors())
.filter(new Predicate<JConstructor>() {
@Override
public boolean apply(JConstructor m) {
return m.isJsConstructor();
}
}).toList();
}
private void checkJsConstructorSubtype(JDeclaredType type) {
if (!isJsConstructorSubtype(type)) {
return;
}
if (Iterables.isEmpty(type.getConstructors())) {
// No constructors in the type; type is not instantiable.
return;
}
if (type.isJsNative()) {
return;
}
JClassType superClass = type.getSuperClass();
JConstructor superPrimaryConsructor = JjsUtils.getPrimaryConstructor(superClass);
if (!superClass.isJsNative() && superPrimaryConsructor == null) {
// Superclass has JsConstructor but does not satisfy the JsConstructor restrictions, no need
// to report more errors.
return;
}
JConstructor primaryConstructor = JjsUtils.getPrimaryConstructor(type);
if (primaryConstructor == null) {
logError(type,
"Class %s should have only one constructor delegating to the superclass since it is "
+ "subclass of a a type with JsConstructor.", getDescription(type));
return;
}
JConstructor delegatedConstructor =
JjsUtils.getDelegatedThisOrSuperConstructor(primaryConstructor);
if (delegatedConstructor.isJsConstructor() ||
delegatedConstructor == superPrimaryConsructor) {
return;
}
logError(primaryConstructor,
"Constructor %s can only delegate to super constructor %s since it is a subclass of a "
+ "type with JsConstructor.",
getDescription(primaryConstructor),
getDescription(superPrimaryConsructor));
}
private void checkMember(
JMember member, Map<String, JsMember> localNames, Map<String, JsMember> ownGlobalNames) {
if (member.getEnclosingType().isJsNative()) {
checkMemberOfNativeJsType(member);
}
if (member.needsDynamicDispatch()) {
checkIllegalOverrides(member);
}
if (member instanceof JMethod) {
checkMethodParameters((JMethod) member);
}
if (member.isJsOverlay()) {
checkJsOverlay(member);
return;
}
if (member.canBeReferencedExternally()) {
checkUnusableByJs(member);
}
if (member.getJsMemberType() == JsMemberType.NONE) {
return;
}
if (!checkJsPropertyAccessor(member)) {
return;
}
checkMemberQualifiedJsName(member);
if (isCheckedLocalName(member)) {
checkLocalName(localNames, member);
}
if (isCheckedGlobalName(member)) {
checkGlobalName(ownGlobalNames, member);
}
}
private void checkIllegalOverrides(JMember member) {
if (member instanceof JField) {
return;
}
JMethod method = (JMethod) member;
if (method.isSynthetic()) {
// Ignore synthetic methods. These synthetic methods might be accidental overrides or
// default method implementations, and they forward to the same implementation so it is
// safe to allow them.
return;
}
for (JMethod overriddenMethod : method.getOverriddenMethods()) {
if (overriddenMethod.isSynthetic()) {
// Ignore synthetic methods for a better error message.
continue;
}
if (overriddenMethod.isJsOverlay()) {
logError(member, "Method '%s' cannot override a JsOverlay method '%s'.",
JjsUtils.getReadableDescription(method),
JjsUtils.getReadableDescription(overriddenMethod));
return;
}
}
}
private void checkJsOverlay(JMember member) {
if (member.getEnclosingType().isJsoType() || member.isSynthetic()) {
return;
}
String memberDescription = JjsUtils.getReadableDescription(member);
if (!member.getEnclosingType().isJsNative() && !member.getEnclosingType().isJsFunction()) {
logError(member,
"JsOverlay '%s' can only be declared in a native type or a JsFunction interface.",
memberDescription);
}
if (member instanceof JConstructor) {
logError(member, "JsOverlay method '%s' cannot be a constructor.", memberDescription);
return;
}
if (member.getJsMemberType() != JsMemberType.NONE) {
logError(member, "JsOverlay method '%s' cannot be nor override a JsProperty or a JsMethod.",
memberDescription);
return;
}
if (member instanceof JField) {
JField field = (JField) member;
if (field.needsDynamicDispatch()) {
logError(member, "JsOverlay field '%s' can only be static.", memberDescription);
}
return;
}
JMethod method = (JMethod) member;
assert method.getOverriddenMethods().isEmpty();
if (method.getBody() == null
|| (!method.isFinal()
&& !method.getEnclosingType().isFinal()
&& !method.isPrivate()
&& !method.isStatic()
&& !method.isDefaultMethod())) {
logError(member, "JsOverlay method '%s' cannot be non-final nor native.", memberDescription);
}
}
private void checkSuperDispachToNativeJavaLangObjectMethodOverride() {
new JVisitor() {
JClassType superClass;
@Override
public boolean visit(JDeclaredType x, Context ctx) {
superClass = JjsUtils.getNativeSuperClassOrNull(x);
// Only examine code in non native subclasses of native JsTypes.
return x instanceof JClassType && superClass != null;
}
@Override
public boolean visit(JMethod x, Context ctx) {
// Do not report errors from synthetic method bodies, those errors are reported
// explicitly elsewhere.
return !x.isSynthetic();
}
@Override
public void endVisit(JMethodCall x, Context ctx) {
JMethod target = x.getTarget();
if (!x.isStaticDispatchOnly()) {
// Not a super call, allow.
return;
}
assert (!target.isStatic());
// Forbid calling through super when the target is the native implementation because
// it might not exist in the native supertype at runtime.
// TODO(rluble): lift this restriction by dispatching through a trampoline. Not that this
// trampoline is different that the one created for non static dispatches.
if ((overridesObjectMethod(target) && target.getEnclosingType().isJsNative())
|| target.getEnclosingType() == jprogram.getTypeJavaLangObject()) {
logError(x, "Cannot use super to call '%s.%s'. 'java.lang.Object' methods in native "
+ "JsTypes cannot be called using super.",
JjsUtils.getReadableDescription(superClass),
target.getName());
return;
}
}
}.accept(jprogram);
}
private void checkMemberOfNativeJsType(JMember member) {
if (member instanceof JMethod && ((JMethod) member).isJsniMethod()) {
logError(member, "JSNI method %s is not allowed in a native JsType.",
getMemberDescription(member));
return;
}
if (member.isSynthetic() || member.isJsOverlay()) {
return;
}
if (overridesObjectMethod(member)) {
if (member.getJsMemberType() != JsMemberType.METHOD
|| !member.getName().equals(member.getJsName())) {
logError(member,
"Method %s cannot override a method from 'java.lang.Object' and change its name.",
getMemberDescription(member));
return;
}
}
JsMemberType jsMemberType = member.getJsMemberType();
switch (jsMemberType) {
case CONSTRUCTOR:
if (!isConstructorEmpty((JConstructor) member)) {
logError(member, "Native JsType constructor %s cannot have non-empty method body.",
getMemberDescription(member));
}
break;
case METHOD:
case GETTER:
case SETTER:
case UNDEFINED_ACCESSOR:
JMethod method = (JMethod) member;
if (!method.isAbstract() && method.getBody() != null) {
logError(member, "Native JsType method %s should be native or abstract.",
getMemberDescription(member));
}
break;
case PROPERTY:
JField field = (JField) member;
if (field.isFinal()) {
logError(member, "Native JsType field %s cannot be final.",
getMemberDescription(member));
} else if (field.hasInitializer()) {
logError(member, "Native JsType field %s cannot have initializer.",
getMemberDescription(member));
}
break;
case NONE:
logError(member, "Native JsType member %s cannot have @JsIgnore.",
getMemberDescription(member));
break;
}
}
private boolean overridesObjectMethod(JMember member) {
if (!(member instanceof JMethod)) {
return false;
}
JMethod method = (JMethod) member;
for (JMethod overriddenMethod : method.getOverriddenMethods()) {
if (overriddenMethod.getEnclosingType() == jprogram.getTypeJavaLangObject()) {
return true;
}
}
return false;
}
private void checkMethodParameters(JMethod method) {
boolean hasOptionalParameters = false;
for (JParameter parameter : method.getParams()) {
if (parameter.isOptional()) {
if (parameter.getType().isPrimitiveType()) {
logError(method, "JsOptional parameter '%s' in method %s cannot be of primitive type.",
parameter.getName(), getMemberDescription(method));
}
hasOptionalParameters = true;
continue;
}
if (hasOptionalParameters && !parameter.isVarargs()) {
logError(method, "JsOptional parameter '%s' in method %s cannot precede parameters "
+ "that are not optional.", parameter.getName(), getMemberDescription(method));
break;
}
}
if (hasOptionalParameters
&& method.getJsMemberType() != JsMemberType.CONSTRUCTOR
&& method.getJsMemberType() != JsMemberType.METHOD
&& !method.isOrOverridesJsFunctionMethod()) {
logError(method, "Method %s has JsOptional parameters and is not a JsMethod, "
+ "a JsConstructor or a JsFunction method.", getMemberDescription(method));
}
if (method.isJsMethodVarargs()) {
checkJsVarargs(method);
}
// Check that parameters that are declared JsOptional in overridden methods remain JsOptional.
for (JMethod overriddenMethod : method.getOverriddenMethods()) {
for (int i = 0; i < overriddenMethod.getParams().size(); i++) {
if (overriddenMethod.getParams().get(i).isOptional()) {
if (!method.getParams().get(i).isOptional()) {
logError(method, "Method %s should declare parameter '%s' as JsOptional",
getMemberDescription(method), method.getParams().get(i).getName());
return;
}
break;
}
}
}
}
private void checkJsVarargs(final JMethod method) {
if (!method.isJsniMethod()) {
return;
}
final JsFunction function = ((JsniMethodBody) method.getBody()).getFunc();
final JsParameter varargParameter = Iterables.getLast(function.getParameters());
new JsVisitor() {
@Override
public void endVisit(JsNameRef x, JsContext ctx) {
if (x.getName() == varargParameter.getName()) {
logError(x, "Cannot access vararg parameter '%s' from JSNI in JsMethod %s."
+ " Use 'arguments' instead.", x.getIdent(),
getMemberDescription(method));
}
}
}.accept(function);
}
private boolean checkJsPropertyAccessor(JMember member) {
JsMemberType memberType = member.getJsMemberType();
if (member.getJsName().equals(JsInteropUtil.INVALID_JSNAME)) {
assert memberType.isPropertyAccessor();
logError(
member,
"JsProperty %s should either follow Java Bean naming conventions or provide a name.",
getMemberDescription(member));
return false;
}
switch (memberType) {
case UNDEFINED_ACCESSOR:
logError(member, "JsProperty %s should have a correct setter or getter signature.",
getMemberDescription(member));
break;
case GETTER:
if (member.getType() != JPrimitiveType.BOOLEAN && member.getName().startsWith("is")) {
logError(member, "JsProperty %s cannot have a non-boolean return.",
getMemberDescription(member));
}
break;
case SETTER:
if (((JMethod) member).getParams().get(0).isVarargs()) {
logError(member, "JsProperty %s cannot have a vararg parameter.",
getMemberDescription(member));
}
break;
}
if (memberType.isPropertyAccessor() && member.isStatic() && !member.isJsNative()) {
logError(member, "Static property accessor '%s' can only be native.",
JjsUtils.getReadableDescription(member));
}
return true;
}
private void checkMemberQualifiedJsName(JMember member) {
if (member instanceof JConstructor) {
// Constructors always inherit their name and namespace from the enclosing type.
// The corresponding checks are done for the type separately.
return;
}
checkJsName(member);
if (member.getJsNamespace().equals(member.getEnclosingType().getQualifiedJsName())) {
// Namespace set by the enclosing type has already been checked.
return;
}
if (member.needsDynamicDispatch()) {
logError(member, "Instance member %s cannot declare a namespace.",
getMemberDescription(member));
return;
}
checkJsNamespace(member);
}
private <T extends HasJsName & HasSourceInfo & CanBeJsNative> void checkJsName(T item) {
if (item.getJsName().isEmpty()) {
logError(item, "%s cannot have an empty name.", getDescription(item));
} else if ((item.isJsNative() && !JsUtils.isValidJsQualifiedName(item.getJsName()))
|| (!item.isJsNative() && !JsUtils.isValidJsIdentifier(item.getJsName()))) {
// Allow qualified names in the name field for JsPackage.GLOBAL native items for future
// compatibility
logError(item, "%s has invalid name '%s'.", getDescription(item), item.getJsName());
}
}
private <T extends HasJsName & HasSourceInfo & CanBeJsNative> void checkJsNamespace(T item) {
if (JsInteropUtil.isGlobal(item.getJsNamespace())) {
return;
}
if (JsInteropUtil.isWindow(item.getJsNamespace())) {
if (item.isJsNative()) {
return;
}
logError(item, "'%s' can only be used as a namespace of native types and members.",
item.getJsNamespace());
} else if (item.getJsNamespace().isEmpty()) {
logError(item, "%s cannot have an empty namespace.", getDescription(item));
} else if (!JsUtils.isValidJsQualifiedName(item.getJsNamespace())) {
logError(item, "%s has invalid namespace '%s'.", getDescription(item), item.getJsNamespace());
}
}
private void checkLocalName(Map<String, JsMember> localNames, JMember member) {
Pair<JsMember, JsMember> oldAndNewJsMember = updateJsMembers(localNames, member);
JsMember oldJsMember = oldAndNewJsMember.left;
JsMember newJsMember = oldAndNewJsMember.right;
checkNameConsistency(member);
checkJsPropertyConsistency(member, newJsMember);
if (oldJsMember == null || oldJsMember == newJsMember) {
return;
}
if (oldJsMember.isJsNative() && newJsMember.isJsNative()) {
return;
}
logError(member, "%s and %s cannot both use the same JavaScript name '%s'.",
getMemberDescription(member), getMemberDescription(oldJsMember.member), member.getJsName());
}
private void checkGlobalName(Map<String, JsMember> ownGlobalNames, JMember member) {
Pair<JsMember, JsMember> oldAndNewJsMember = updateJsMembers(ownGlobalNames, member);
JsMember oldJsMember = oldAndNewJsMember.left;
JsMember newJsMember = oldAndNewJsMember.right;
if (oldJsMember == newJsMember) {
// We allow setter-getter to share the name if they are both defined in the same class, so
// skipping the global name check. However still need to do a consistency check.
checkJsPropertyConsistency(member, newJsMember);
return;
}
String currentGlobalNameDescription =
minimalRebuildCache.addExportedGlobalName(member.getQualifiedJsName(),
JjsUtils.getReadableDescription(member), member.getEnclosingType().getName());
if (currentGlobalNameDescription == null) {
return;
}
logError(member, "%s cannot be exported because the global name '%s' is already taken by '%s'.",
getMemberDescription(member), member.getQualifiedJsName(), currentGlobalNameDescription);
}
private void checkJsPropertyConsistency(JMember member, JsMember newMember) {
if (newMember.setter != null && newMember.getter != null) {
List<JParameter> setterParams = ((JMethod) newMember.setter).getParams();
if (newMember.getter.getType() != setterParams.get(0).getType()) {
logError(member, "JsProperty setter %s and getter %s cannot have inconsistent types.",
getMemberDescription(newMember.setter), getMemberDescription(newMember.getter));
}
}
}
private void checkNameConsistency(JMember member) {
if (member instanceof JMethod) {
String jsName = member.getJsName();
for (JMethod jMethod : ((JMethod) member).getOverriddenMethods()) {
String parentName = jMethod.getJsName();
if (parentName != null && !parentName.equals(jsName)) {
logError(
member,
"%s cannot be assigned a different JavaScript name than the method it overrides.",
getMemberDescription(member));
break;
}
}
}
}
private void checkStaticJsPropertyCalls() {
new JVisitor() {
@Override
public boolean visit(JMethod x, Context ctx) {
// Skip unnecessary synthetic override, as they will not be generated.
return !JjsUtils.isJsMemberUnnecessaryAccidentalOverride(x);
}
@Override
public void endVisit(JMethodCall x, Context ctx) {
JMethod target = x.getTarget();
if (x.isStaticDispatchOnly() && target.getJsMemberType().isPropertyAccessor()) {
logError(x, "Cannot call property accessor %s via super.",
getMemberDescription(target));
}
}
}.accept(jprogram);
}
private void checkInstanceOfNativeJsTypesOrJsFunctionImplementations() {
new JVisitor() {
@Override
public boolean visit(JInstanceOf x, Context ctx) {
JReferenceType type = x.getTestType();
if (type.isJsNative() && type instanceof JInterfaceType) {
logError(x, "Cannot do instanceof against native JsType interface '%s'.",
JjsUtils.getReadableDescription(type));
} else if (type.isJsFunctionImplementation()) {
logError(x, "Cannot do instanceof against JsFunction implementation '%s'.",
JjsUtils.getReadableDescription(type));
}
return true;
}
}.accept(jprogram);
}
private boolean checkJsType(JDeclaredType type) {
// Java (at least up to Java 8) does not allow to annotate anonymous classes or lambdas; if
// it ever becomes possible we should emit an error.
assert type.getClassDisposition() != NestedClassDisposition.ANONYMOUS
&& type.getClassDisposition() != NestedClassDisposition.LAMBDA;
if (type.getClassDisposition() == NestedClassDisposition.LOCAL) {
logError("Local class '%s' cannot be a JsType.", type);
return false;
}
return true;
}
private boolean checkNativeJsType(JDeclaredType type) {
if (type.isEnumOrSubclass() != null) {
logError("Enum '%s' cannot be a native JsType.", type);
return false;
}
if (type.getClassDisposition() == NestedClassDisposition.INNER) {
logError("Non static inner class '%s' cannot be a native JsType.", type);
return false;
}
JClassType superClass = type.getSuperClass();
if (superClass != null && superClass != jprogram.getTypeJavaLangObject() &&
!superClass.isJsNative()) {
logError("Native JsType '%s' can only extend native JsType classes.", type);
}
for (JInterfaceType interfaceType : type.getImplements()) {
if (!interfaceType.isJsNative()) {
logError(type, "Native JsType '%s' can only %s native JsType interfaces.",
getDescription(type),
type instanceof JInterfaceType ? "extend" : "implement");
}
}
if (!isInitEmpty(type)) {
logError("Native JsType '%s' cannot have initializer.", type);
}
return true;
}
private void checkMemberOfJsFunction(JMember member) {
if (member.getJsMemberType() != JsMemberType.NONE) {
logError(member,
"JsFunction interface member '%s' cannot be JsMethod nor JsProperty.",
JjsUtils.getReadableDescription(member));
}
if (member.isJsOverlay() || member.isSynthetic()) {
return;
}
if (member instanceof JMethod && ((JMethod) member).isOrOverridesJsFunctionMethod()) {
return;
}
logError(member, "JsFunction interface '%s' cannot declare non-JsOverlay member '%s'.",
JjsUtils.getReadableDescription(member.getEnclosingType()),
JjsUtils.getReadableDescription(member));
}
private void checkJsFunction(JDeclaredType type) {
if (type.getImplements().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;
}
// Functional interface restriction already enforced by JSORestrictionChecker. It is safe
// to assume here that there is a single abstract method.
for (JMember member : type.getMembers()) {
checkMemberOfJsFunction(member);
}
}
private void checkMemberOfJsFunctionImplementation(JMember member) {
if (member.getJsMemberType() != JsMemberType.NONE) {
logError(member,
"JsFunction implementation member '%s' cannot be JsMethod nor JsProperty.",
JjsUtils.getReadableDescription(member));
}
if (!(member instanceof JMethod)) {
return;
}
JMethod method = (JMethod) member;
if (method.isOrOverridesJsFunctionMethod()
|| method.isSynthetic()
|| method.getOverriddenMethods().isEmpty()) {
return;
}
// Methods that are not effectively static dispatch are disallowed. In this case these
// could only be overridable methods of java.lang.Object, i.e. toString, hashCode and equals.
logError(method, "JsFunction implementation '%s' cannot implement method '%s'.",
JjsUtils.getReadableDescription(member.getEnclosingType()),
JjsUtils.getReadableDescription(method));
}
private void checkJsFunctionImplementation(JDeclaredType type) {
if (!type.isFinal()) {
logError("JsFunction implementation '%s' must be final.",
type);
}
if (type.getImplements().size() != 1) {
logError("JsFunction implementation '%s' cannot implement more than one interface.",
type);
}
if (type.getSuperClass() != jprogram.getTypeJavaLangObject()) {
logError("JsFunction implementation '%s' cannot extend a class.", type);
}
if (type.isJsType()) {
logError("'%s' cannot be both a JsFunction implementation and a JsType at the same time.",
type);
return;
}
for (JMember member : type.getMembers()) {
checkMemberOfJsFunctionImplementation(member);
}
}
private void checkJsFunctionSubtype(JDeclaredType type) {
for (JInterfaceType superInterface : type.getImplements()) {
if (superInterface.isJsFunction()) {
logError(type, "'%s' cannot extend JsFunction '%s'.",
JjsUtils.getReadableDescription(type), JjsUtils.getReadableDescription(superInterface));
}
}
}
private boolean checkProgram(TreeLogger logger) {
for (JDeclaredType type : jprogram.getModuleDeclaredTypes()) {
checkType(type);
}
checkStaticJsPropertyCalls();
checkInstanceOfNativeJsTypesOrJsFunctionImplementations();
checkSuperDispachToNativeJavaLangObjectMethodOverride();
if (wasUnusableByJsWarningReported) {
logSuggestion(
"Suppress \"[unusable-by-js]\" warnings by adding a "
+ "`@SuppressWarnings(\"unusable-by-js\")` annotation to the corresponding member.");
}
boolean hasErrors = reportErrorsAndWarnings(logger);
return !hasErrors;
}
private boolean isJsConstructorSubtype(JDeclaredType type) {
JClassType superClass = type.getSuperClass();
if (superClass == null) {
return false;
}
if (JjsUtils.getJsConstructor(superClass) != null) {
return true;
}
return isJsConstructorSubtype(superClass);
}
private static boolean isSubclassOfNativeClass(JDeclaredType type) {
return JjsUtils.getNativeSuperClassOrNull(type) != null;
}
private void checkType(JDeclaredType type) {
minimalRebuildCache.removeExportedNames(type.getName());
if (type.isJsType()) {
if (!checkJsType(type)) {
return;
}
checkJsName(type);
checkJsNamespace(type);
}
if (type.isJsNative()) {
if (!checkNativeJsType(type)) {
return;
}
} else if (isSubclassOfNativeClass(type)) {
checkSubclassOfNativeClass(type);
}
if (type.isJsFunction()) {
checkJsFunction(type);
} else if (type.isJsFunctionImplementation()) {
checkJsFunctionImplementation(type);
} else {
checkJsFunctionSubtype(type);
checkJsConstructors(type);
checkJsConstructorSubtype(type);
}
Map<String, JsMember> ownGlobalNames = Maps.newHashMap();
Map<String, JsMember> localNames = collectLocalNames(type.getSuperClass());
for (JMember member : type.getMembers()) {
checkMember(member, localNames, ownGlobalNames);
}
}
private void checkSubclassOfNativeClass(JDeclaredType type) {
assert (type instanceof JClassType);
for (JMethod method : type.getMethods()) {
if (!overridesObjectMethod(method) || !method.isSynthetic()) {
continue;
}
// Only look at synthetic (accidental) overrides.
for (JMethod overridenMethod : method.getOverriddenMethods()) {
if (overridenMethod.getEnclosingType() instanceof JInterfaceType
&& overridenMethod.getJsMemberType() != JsMemberType.METHOD) {
logError(
type,
"Native JsType subclass %s can not implement interface %s that declares method '%s' "
+ "inherited from java.lang.Object.",
getDescription(type),
getDescription(overridenMethod.getEnclosingType()),
overridenMethod.getName());
}
}
}
}
private void checkUnusableByJs(JMember member) {
logIfUnusableByJs(member, member instanceof JField ? "Type of" : "Return type of", member);
if (member instanceof JMethod) {
for (JParameter parameter : ((JMethod) member).getParams()) {
String prefix = String.format("Type of parameter '%s' in", parameter.getName());
logIfUnusableByJs(parameter, prefix, member);
}
}
}
private <T extends HasType & CanHaveSuppressedWarnings> void logIfUnusableByJs(
T hasType, String prefix, JMember x) {
if (hasType.getType().canBeReferencedExternally()) {
return;
}
if (isUnusableByJsSuppressed(x.getEnclosingType()) || isUnusableByJsSuppressed(x)
|| isUnusableByJsSuppressed(hasType)) {
return;
}
logWarning(x, "[unusable-by-js] %s %s is not usable by but exposed to JavaScript.", prefix,
getMemberDescription(x));
wasUnusableByJsWarningReported = true;
}
private static class JsMember {
private JMember member;
private JMember setter;
private JMember getter;
public JsMember(JMember member) {
this.member = member;
}
public JsMember(JMember member, JMember setter, JMember getter) {
this.member = member;
this.setter = setter;
this.getter = getter;
}
public boolean isJsNative() {
return member.isJsNative();
}
public boolean isPropertyAccessor() {
return setter != null || getter != null;
}
}
private LinkedHashMap<String, JsMember> collectLocalNames(JDeclaredType type) {
if (type == null) {
return Maps.newLinkedHashMap();
}
LinkedHashMap<String, JsMember> memberByLocalMemberNames =
collectLocalNames(type.getSuperClass());
for (JMember member : type.getMembers()) {
if (isCheckedLocalName(member)) {
updateJsMembers(memberByLocalMemberNames, member);
}
}
return memberByLocalMemberNames;
}
private boolean isCheckedLocalName(JMember method) {
return method.needsDynamicDispatch() && method.getJsMemberType() != JsMemberType.NONE
&& !isSyntheticBridgeMethod(method);
}
private boolean isSyntheticBridgeMethod(JMember member) {
if (!(member instanceof JMethod)) {
return false;
}
// 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 member.isSynthetic() && !((JMethod) member).isForwarding();
}
private boolean isCheckedGlobalName(JMember member) {
return !member.needsDynamicDispatch() && !member.isJsNative();
}
private Pair<JsMember, JsMember> updateJsMembers(
Map<String, JsMember> memberByNames, JMember member) {
JsMember oldJsMember = memberByNames.get(member.getJsName());
JsMember newJsMember = createOrUpdateJsMember(oldJsMember, member);
memberByNames.put(member.getJsName(), newJsMember);
return Pair.create(oldJsMember, newJsMember);
}
private JsMember createOrUpdateJsMember(JsMember jsMember, JMember member) {
switch (member.getJsMemberType()) {
case GETTER:
if (jsMember != null && jsMember.isPropertyAccessor()) {
if (jsMember.getter == null || overrides(member, jsMember.getter)) {
jsMember.getter = member;
jsMember.member = member;
return jsMember;
}
}
return new JsMember(member, jsMember == null ? null : jsMember.setter, member);
case SETTER:
if (jsMember != null && jsMember.isPropertyAccessor()) {
if (jsMember.setter == null || overrides(member, jsMember.setter)) {
jsMember.setter = member;
jsMember.member = member;
return jsMember;
}
}
return new JsMember(member, member, jsMember == null ? null : jsMember.getter);
default:
if (jsMember != null && !jsMember.isPropertyAccessor()) {
if (overrides(member, jsMember.member)) {
jsMember.member = member;
return jsMember;
}
}
return new JsMember(member);
}
}
private boolean overrides(JMember member, JMember potentiallyOverriddenMember) {
if (member instanceof JField || potentiallyOverriddenMember instanceof JField) {
return false;
}
JMethod method = (JMethod) member;
if (method.getOverriddenMethods().contains(potentiallyOverriddenMember)) {
return true;
}
// Consider methods that have the same name and parameter signature to be overrides.
// GWT models overrides similar to the JVM (not Java) in the sense that for a method to override
// another they must have identical signatures (includes parameters and return type).
// Methods that only differ in return types are Java overrides and need to be considered so
// for local name collision checking.
JMethod potentiallyOverriddenMethod = (JMethod) potentiallyOverriddenMember;
// TODO(goktug): make this more precise to handle package visibilities.
boolean visibilitiesMatchesForOverride =
!method.isPackagePrivate() && !method.isPrivate()
&& !potentiallyOverriddenMethod.isPackagePrivate()
&& !potentiallyOverriddenMethod.isPrivate();
return visibilitiesMatchesForOverride
&& method.getJsniSignature(false, false)
.equals(potentiallyOverriddenMethod.getJsniSignature(false, false));
}
private boolean isUnusableByJsSuppressed(CanHaveSuppressedWarnings x) {
return x.getSuppressedWarnings() != null &&
x.getSuppressedWarnings().contains(JsInteropUtil.UNUSABLE_BY_JS);
}
}