All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gwt.dev.jjs.impl.JsInteropRestrictionChecker Maven / Gradle / Ivy

There is a newer version: 2.10.0
Show newest version
/*
 * 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() {
      @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 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 getJsConstructors(JDeclaredType type) {
    return FluentIterable
          .from(type.getConstructors())
          .filter(new Predicate() {
            @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 localNames, Map 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()) {
      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  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  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 localNames, JMember member) {
    Pair 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 ownGlobalNames, JMember member) {
    Pair 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 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 ownGlobalNames = Maps.newHashMap();
    Map 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  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 collectLocalNames(JDeclaredType type) {
    if (type == null) {
      return Maps.newLinkedHashMap();
    }

    LinkedHashMap 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 updateJsMembers(
      Map 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);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy