blob: f31c775e60db022f69800ede7cb670c7afc73ff3 [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.JType;
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.thirdparty.guava.common.base.Preconditions;
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.LinkedHashMultimap;
import com.google.gwt.thirdparty.guava.common.collect.Multimap;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import java.util.List;
import java.util.Set;
/**
* 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, Multimap<String, JMember> instanceMembersByJsName) {
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 (isInstanceJsMember(member)) {
checkInstanceNameConsistency(instanceMembersByJsName, member);
}
if (isStaticJsMember(member)) {
checkStaticNameCollisions(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) {
if (method.isSynthetic()) {
return;
}
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 checkInstanceNameConsistency(
Multimap<String, JMember> instanceMembersByJsName, JMember member) {
checkOverrideConsistency(member);
if (member.isJsNative()) {
return;
}
String name = member.getJsName();
Set<JMember> potentiallyCollidingMembers =
Sets.newLinkedHashSet(instanceMembersByJsName.get(name));
// Remove self.
boolean removed = potentiallyCollidingMembers.remove(member);
Preconditions.checkState(removed);
// Remove native members as they don't cause collisions.
Iterables.removeIf(potentiallyCollidingMembers,
new Predicate<JMember>() {
@Override
public boolean apply(JMember member) {
return member.isJsNative();
}
});
if (potentiallyCollidingMembers.isEmpty()) {
// No colliding members.
return;
}
JMember potentiallyCollidingMember = potentiallyCollidingMembers.iterator().next();
if (potentiallyCollidingMembers.size() == 1
&& isJsPropertyAccessorPair(member, potentiallyCollidingMember)) {
if (!checkPropertyConsistency(member, potentiallyCollidingMember)) {
// Remove colliding member to avoid duplicate error messages.
instanceMembersByJsName.get(name).remove(member);
}
return;
}
logError(member,
"%s and %s cannot both use the same JavaScript name '%s'.",
getMemberDescription(member),
getMemberDescription(potentiallyCollidingMember),
member.getJsName());
// Remove colliding member avoid duplicate error messages.
instanceMembersByJsName.get(name).remove(member);
}
private boolean isJsPropertyAccessorPair(JMember thisMember, JMember thatMember) {
return (thisMember.getJsMemberType() == JsMemberType.GETTER
&& thatMember.getJsMemberType() == JsMemberType.SETTER)
|| (thatMember.getJsMemberType() == JsMemberType.GETTER
&& thisMember.getJsMemberType() == JsMemberType.SETTER);
}
private void checkStaticNameCollisions(JMember member) {
if (member.isJsNative()) {
return;
}
// TODO(rluble): Add static property consistency check here if static property accessors are
// ever allowed.
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 boolean checkPropertyConsistency(JMember member, JMember otherMember) {
JMember setter = member.getJsMemberType() == JsMemberType.SETTER ? member : otherMember;
JMember getter = member.getJsMemberType() == JsMemberType.GETTER ? member : otherMember;
if (setter != null && getter != null) {
List<JParameter> setterParams = ((JMethod) setter).getParams();
if (isSameType(getter.getType(), setterParams.get(0).getType())) {
logError(member, "JsProperty setter %s and getter %s cannot have inconsistent types.",
getMemberDescription(setter), getMemberDescription(getter));
return false;
}
}
return true;
}
/**
* Returns true if {@code thisType} is the same type as {@code thatType}.
*/
private boolean isSameType(JType thisType, JType thatType) {
// NOTE: The comparison here is made by signature instead of reference equality because under
// incremental compilation this types might be reference only and hence not unique.
return !thisType.getJavahSignatureName().equals(thatType.getJavahSignatureName());
}
private void checkOverrideConsistency(JMember member) {
if (member instanceof JMethod) {
String jsName = member.getJsName();
for (JMethod overridenMethod : ((JMethod) member).getOverriddenMethods()) {
String parentName = overridenMethod.getJsName();
if (parentName == null) {
continue;
}
if (!parentName.equals(jsName)) {
logError(
member,
"%s cannot be assigned a different JavaScript name than the method it overrides.",
getMemberDescription(member));
break;
}
if (overridenMethod.getJsMemberType() != member.getJsMemberType()) {
// Overrides can not change JsMethod to JsProperty nor vice versa.
logError(
member,
"%s %s cannot override %s %s.",
member.getJsMemberType() == JsMemberType.METHOD ? "JsMethod" : "JsProperty",
getMemberDescription(member),
overridenMethod.getJsMemberType() == JsMemberType.METHOD ? "JsMethod" : "JsProperty",
getMemberDescription(overridenMethod));
}
}
}
}
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 checkJsNameOnType(JDeclaredType type) {
if (!type.getJsName().equals("*") && !type.getJsName().equals("?")) {
checkJsName(type);
return;
}
if (!type.isJsNative()
|| !(type instanceof JInterfaceType)
|| !JsInteropUtil.isGlobal(type.getJsNamespace())) {
logError(type,
"'%s' can only be used as a name for native interfaces in the global namespace.",
type.getJsName());
}
}
private void checkType(JDeclaredType type) {
minimalRebuildCache.removeExportedNames(type.getName());
if (type.isJsType()) {
if (!checkJsType(type)) {
return;
}
checkJsNameOnType(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);
}
Multimap<String, JMember> instanceJsNames = collectInstanceMembersByJsNames(type);
for (JMember member : type.getMembers()) {
checkMember(member, instanceJsNames);
}
}
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) {
if (member instanceof JMethod) {
JMethod method = (JMethod) member;
if (method.isSynthetic() && !method.isSyntheticAccidentalOverride()) {
// Do not emit warnings for synthetic methods unless they are accidental overrides.
return;
}
logIfUnusableByJs(member, "Return type of", member);
for (JParameter parameter : method.getParams()) {
String prefix = String.format("Type of parameter '%s' in", parameter.getName());
logIfUnusableByJs(parameter, prefix, member);
}
} else {
// it's a field
logIfUnusableByJs(member, "Type of", 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 Multimap<String, JMember> collectInstanceMembersByJsNames(JDeclaredType type) {
if (type == null) {
return LinkedHashMultimap.create();
}
Multimap<String, JMember> instanceMembersByJsName =
collectInstanceMembersByJsNames(type.getSuperClass());
for (JMember member : type.getMembers()) {
if (isInstanceJsMember(member)) {
addMember(instanceMembersByJsName, member);
}
}
return instanceMembersByJsName;
}
private static void addMember(
Multimap<String, JMember> instanceMembersByJsName, final JMember member) {
String name = member.getJsName();
Iterables.removeIf(instanceMembersByJsName.get(name),
new Predicate<JMember>() {
@Override
public boolean apply(JMember m) {
return overrides(member, m);
}
});
instanceMembersByJsName.put(name, member);
}
private boolean isInstanceJsMember(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 isStaticJsMember(JMember member) {
return !member.needsDynamicDispatch() && member.getJsMemberType() != JsMemberType.NONE;
}
private static 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 instance 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);
}
}