public JavaFileFeatures extractFeaturesFromJavaCode()

in src/com/facebook/buck/jvm/java/JavaFileParser.java [486:871]


  public JavaFileFeatures extractFeaturesFromJavaCode(String code) {
    // For now, we will harcode this. Ultimately, we probably want to make this configurable via
    // .buckconfig. For example, the Buck project itself is diligent about disallowing wildcard
    // imports, but the one exception is the Java code generated via Thrift in src-gen.
    boolean shouldThrowForUnsupportedWildcardImport = false;

    AtomicBoolean isPoisonedByUnsupportedWildcardImport = new AtomicBoolean(false);

    CompilationUnit compilationUnit = makeCompilationUnitFromSource(code);

    ImmutableSortedSet.Builder<String> providedSymbols = ImmutableSortedSet.naturalOrder();
    ImmutableSortedSet.Builder<String> requiredSymbols = ImmutableSortedSet.naturalOrder();
    ImmutableSortedSet.Builder<String> exportedSymbols = ImmutableSortedSet.naturalOrder();
    ImmutableSortedSet.Builder<String> requiredSymbolsFromExplicitImports =
        ImmutableSortedSet.naturalOrder();

    compilationUnit.accept(
        new ASTVisitor() {

          @Nullable private String packageName;

          /** Maps simple name to fully-qualified name. */
          private Map<String, String> simpleImportedTypes = new HashMap<>();

          /**
           * Maps wildcard import prefixes, such as {@code "java.util"} to the types in the
           * respective package if a wildcard import such as {@code import java.util.*} is used.
           */
          private Map<String, ImmutableSet<String>> wildcardImports = new HashMap<>();

          @Override
          public boolean visit(PackageDeclaration node) {
            Preconditions.checkState(
                packageName == null, "There should be at most one package declaration");
            packageName = node.getName().getFullyQualifiedName();
            return false;
          }

          // providedSymbols

          @Override
          public boolean visit(TypeDeclaration node) {
            // Local classes can be declared inside of methods. Skip over these.
            if (node.getParent() instanceof TypeDeclarationStatement) {
              return true;
            }

            String fullyQualifiedName = getFullyQualifiedTypeName(node);
            if (fullyQualifiedName != null) {
              providedSymbols.add(fullyQualifiedName);
            }

            @SuppressWarnings("unchecked")
            List<Type> interfaceTypes = node.superInterfaceTypes();
            for (Type interfaceType : interfaceTypes) {
              tryAddType(interfaceType, DependencyType.EXPORTED);
            }

            Type superclassType = node.getSuperclassType();
            if (superclassType != null) {
              tryAddType(superclassType, DependencyType.EXPORTED);
            }

            return true;
          }

          @Override
          public boolean visit(EnumDeclaration node) {
            String fullyQualifiedName = getFullyQualifiedTypeName(node);
            if (fullyQualifiedName != null) {
              providedSymbols.add(fullyQualifiedName);
            }
            return true;
          }

          @Override
          public boolean visit(AnnotationTypeDeclaration node) {
            String fullyQualifiedName = getFullyQualifiedTypeName(node);
            if (fullyQualifiedName != null) {
              providedSymbols.add(fullyQualifiedName);
            }
            return true;
          }

          // requiredSymbols

          /**
           * Uses heuristics to try to figure out what type of QualifiedName this is. Returns a
           * non-null value if this is believed to be a reference that qualifies as a "required
           * symbol" relationship.
           */
          @Override
          public boolean visit(QualifiedName node) {
            QualifiedName ancestor = findMostQualifiedAncestor(node);
            ASTNode parent = ancestor.getParent();
            if (!(parent instanceof PackageDeclaration) && !(parent instanceof ImportDeclaration)) {
              String symbol = ancestor.getFullyQualifiedName();

              // If it does not start with an uppercase letter, it is probably because it is a
              // property lookup.
              if (CharMatcher.javaUpperCase().matches(symbol.charAt(0))) {
                addTypeFromDotDelimitedSequence(symbol, DependencyType.REQUIRED);
              }
            }

            return false;
          }

          /**
           * @param expr could be "Example", "Example.field", "com.example.Example". Note it could
           *     also be a built-in type, such as "java.lang.Integer", in which case it will not be
           *     added to the set of required symbols.
           */
          private void addTypeFromDotDelimitedSequence(String expr, DependencyType dependencyType) {
            // At this point, symbol could be `System.out`. We want to reduce it to `System` and
            // then check it against JAVA_LANG_TYPES.
            if (startsWithUppercaseChar(expr)) {
              int index = expr.indexOf('.');
              if (index >= 0) {
                String leftmostComponent = expr.substring(0, index);
                if (JAVA_LANG_TYPES.contains(leftmostComponent)) {
                  return;
                }
              }
            }

            expr = qualifyWithPackageNameIfNecessary(expr);
            addSymbol(expr, dependencyType);
          }

          @Override
          public boolean visit(ImportDeclaration node) {
            String fullyQualifiedName = node.getName().getFullyQualifiedName();

            // Apparently, "on demand" means "uses a wildcard," such as "import java.util.*".
            // Although we can choose to prohibit these in our own code, it is much harder to
            // enforce for third-party code. As such, we will tolerate these for some of the common
            // cases.
            if (node.isOnDemand()) {
              ImmutableSet<String> value = SUPPORTED_WILDCARD_IMPORTS.get(fullyQualifiedName);
              if (value != null) {
                wildcardImports.put(fullyQualifiedName, value);
                return false;
              } else if (shouldThrowForUnsupportedWildcardImport) {
                throw new RuntimeException(
                    String.format(
                        "Use of wildcard 'import %s.*' makes it impossible to statically determine "
                            + "required symbols in this file. Please enumerate explicit imports.",
                        fullyQualifiedName));
              } else {
                isPoisonedByUnsupportedWildcardImport.set(true);
                return false;
              }
            }

            // Only worry about the dependency on the enclosing type.
            Optional<String> simpleName = getSimpleNameFromFullyQualifiedName(fullyQualifiedName);
            if (simpleName.isPresent()) {
              String name = simpleName.get();
              int index = fullyQualifiedName.indexOf("." + name);
              String enclosingType = fullyQualifiedName.substring(0, index + name.length() + 1);
              requiredSymbolsFromExplicitImports.add(enclosingType);

              simpleImportedTypes.put(name, enclosingType);
            } else {
              LOG.info("Suspicious import lacks obvious enclosing type: %s", fullyQualifiedName);
              // The one example we have seen of this in the wild is
              // "org.whispersystems.curve25519.java.curve_sigs". In practice, we still need to add
              // it as a required symbol in this case.
              requiredSymbols.add(fullyQualifiedName);
            }
            return false;
          }

          @Override
          public boolean visit(MethodInvocation node) {
            if (node.getExpression() == null) {
              return true;
            }

            String receiver = node.getExpression().toString();
            if (looksLikeAType(receiver)) {
              addTypeFromDotDelimitedSequence(receiver, DependencyType.REQUIRED);
            }
            return true;
          }

          /** An annotation on a member with zero arguments. */
          @Override
          public boolean visit(MarkerAnnotation node) {
            DependencyType dependencyType = findDependencyTypeForAnnotation(node);
            addSimpleTypeName(node.getTypeName(), dependencyType);
            return true;
          }

          /** An annotation on a member with named arguments. */
          @Override
          public boolean visit(NormalAnnotation node) {
            DependencyType dependencyType = findDependencyTypeForAnnotation(node);
            addSimpleTypeName(node.getTypeName(), dependencyType);
            return true;
          }

          /** An annotation on a member with a single, unnamed argument. */
          @Override
          public boolean visit(SingleMemberAnnotation node) {
            DependencyType dependencyType = findDependencyTypeForAnnotation(node);
            addSimpleTypeName(node.getTypeName(), dependencyType);
            return true;
          }

          private DependencyType findDependencyTypeForAnnotation(Annotation annotation) {
            ASTNode parentNode = annotation.getParent();
            if (parentNode == null) {
              return DependencyType.REQUIRED;
            }

            if (parentNode instanceof BodyDeclaration) {
              // Note that BodyDeclaration is an abstract class. Its subclasses are things like
              // FieldDeclaration and MethodDeclaration. We want to be sure that an annotation on
              // any non-private declaration is considered an exported symbol.
              BodyDeclaration declaration = (BodyDeclaration) parentNode;

              int modifiers = declaration.getModifiers();
              if ((modifiers & Modifier.PRIVATE) == 0) {
                return DependencyType.EXPORTED;
              }
            }
            return DependencyType.REQUIRED;
          }

          @Override
          public boolean visit(SimpleType node) {
            // This method is responsible for finding the overwhelming majority of the required
            // symbols in the AST.
            tryAddType(node, DependencyType.REQUIRED);
            return true;
          }

          // exportedSymbols

          @Override
          public boolean visit(MethodDeclaration node) {
            // Types from private method signatures need not be exported.
            if ((node.getModifiers() & Modifier.PRIVATE) != 0) {
              return true;
            }

            Type returnType = node.getReturnType2();
            if (returnType != null) {
              tryAddType(returnType, DependencyType.EXPORTED);
            }

            @SuppressWarnings("unchecked")
            List<SingleVariableDeclaration> params = node.parameters();
            for (SingleVariableDeclaration decl : params) {
              tryAddType(decl.getType(), DependencyType.EXPORTED);
            }

            @SuppressWarnings("unchecked")
            List<Type> exceptions = node.thrownExceptionTypes();
            for (Type exception : exceptions) {
              tryAddType(exception, DependencyType.EXPORTED);
            }

            return true;
          }

          @Override
          public boolean visit(FieldDeclaration node) {
            // Types from private fields need not be exported.
            if ((node.getModifiers() & Modifier.PRIVATE) == 0) {
              tryAddType(node.getType(), DependencyType.EXPORTED);
            }
            return true;
          }

          private void tryAddType(Type type, DependencyType dependencyType) {
            if (type.isSimpleType()) {
              SimpleType simpleType = (SimpleType) type;
              Name simpleTypeName = simpleType.getName();
              String simpleName = simpleTypeName.toString();

              // For a Type such as IExample<T>, both "IExample" and "T" will be submitted here as
              // simple types. As such, we use this imperfect heuristic to filter out "T" from being
              // added. Note that this will erroneously exclude "URI". In practice, this should
              // generally be OK. For example, assuming "URI" is also imported, then at least it
              // will end up in the set of required symbols. To this end, we perform a second check
              // for "all caps" types to see if there is a corresponding import and if it should be
              // exported rather than simply required.
              if (!CharMatcher.javaUpperCase().matchesAllOf(simpleName)
                  || (dependencyType == DependencyType.EXPORTED
                      && simpleImportedTypes.containsKey(simpleName))) {
                addSimpleTypeName(simpleTypeName, dependencyType);
              }
            } else if (type.isArrayType()) {
              ArrayType arrayType = (ArrayType) type;
              tryAddType(arrayType.getElementType(), dependencyType);
            } else if (type.isParameterizedType()) {
              ParameterizedType parameterizedType = (ParameterizedType) type;
              tryAddType(parameterizedType.getType(), dependencyType);
              @SuppressWarnings("unchecked")
              List<Type> argTypes = parameterizedType.typeArguments();
              for (Type argType : argTypes) {
                tryAddType(argType, dependencyType);
              }
            }
          }

          private void addSimpleTypeName(Name simpleTypeName, DependencyType dependencyType) {
            String simpleName = simpleTypeName.toString();
            if (JAVA_LANG_TYPES.contains(simpleName)) {
              return;
            }

            String fullyQualifiedNameForSimpleName = simpleImportedTypes.get(simpleName);
            if (fullyQualifiedNameForSimpleName != null) {
              // May need to promote from required to exported in this case.
              if (dependencyType == DependencyType.EXPORTED) {
                addSymbol(fullyQualifiedNameForSimpleName, DependencyType.EXPORTED);
              }
              return;
            }

            // For well-behaved source code, this will always be empty, so don't even bother to
            // create the iterator most of the time.
            if (!wildcardImports.isEmpty()) {
              for (Map.Entry<String, ImmutableSet<String>> entry : wildcardImports.entrySet()) {
                Set<String> types = entry.getValue();
                if (types.contains(simpleName)) {
                  String packageName = entry.getKey();
                  addSymbol(packageName + "." + simpleName, dependencyType);
                  return;
                }
              }
            }

            String symbol = simpleTypeName.getFullyQualifiedName();
            symbol = qualifyWithPackageNameIfNecessary(symbol);
            addSymbol(symbol, dependencyType);
          }

          private void addSymbol(String symbol, DependencyType dependencyType) {
            ((dependencyType == DependencyType.REQUIRED) ? requiredSymbols : exportedSymbols)
                .add(symbol);
          }

          private String qualifyWithPackageNameIfNecessary(String symbol) {
            if (!startsWithUppercaseChar(symbol)) {
              return symbol;
            }

            // If the symbol starts with a capital letter, then we assume that it is a reference to
            // a type in the same package.
            int index = symbol.indexOf('.');
            if (index >= 0) {
              symbol = symbol.substring(0, index);
            }
            if (packageName != null) {
              symbol = packageName + "." + symbol;
            }

            return symbol;
          }
        });

    // TODO(mbolin): Special treatment for exportedSymbols when poisoned by wildcard import.
    ImmutableSortedSet<String> totalExportedSymbols = exportedSymbols.build();

    // If we were poisoned by an unsupported wildcard import, then we should rely exclusively on
    // the explicit imports to determine the required symbols.
    Set<String> totalRequiredSymbols = new HashSet<>();
    if (isPoisonedByUnsupportedWildcardImport.get()) {
      totalRequiredSymbols.addAll(requiredSymbolsFromExplicitImports.build());
    } else {
      totalRequiredSymbols.addAll(requiredSymbolsFromExplicitImports.build());
      totalRequiredSymbols.addAll(requiredSymbols.build());
    }
    // Make sure that required and exported symbols are disjoint sets.
    totalRequiredSymbols.removeAll(totalExportedSymbols);

    return new JavaFileFeatures(
        providedSymbols.build(),
        ImmutableSortedSet.copyOf(totalRequiredSymbols),
        totalExportedSymbols);
  }