| /* |
| * 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) { |
| 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 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 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); |
| } |
| |
| 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); |
| } |
| } |