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

com.google.googlejavaformat.java.RemoveUnusedImports Maven / Gradle / Ivy

There is a newer version: 1.25.0
Show newest version
/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.googlejavaformat.java;

import com.google.common.base.CharMatcher;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.collect.Range;
import com.google.common.collect.RangeMap;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeMap;
import com.google.common.collect.TreeRangeSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ArrayType;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jdt.core.dom.MemberRef;
import org.eclipse.jdt.core.dom.MethodRef;
import org.eclipse.jdt.core.dom.MethodRefParameter;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.TagElement;
import org.eclipse.jdt.core.dom.Type;

/**
 * Removes unused imports from a source file. Imports that are only used in javadoc are also
 * removed, and the references in javadoc are replaced with fully qualified names.
 */
public class RemoveUnusedImports {

  /** Configuration for javadoc-only imports. */
  public enum JavadocOnlyImports {
    /** Remove imports that are only used in javadoc, and fully qualify any {@code @link} tags. */
    REMOVE,
    /** Keep imports that are only used in javadoc. */
    KEEP
  }

  // Visits an AST, recording all simple names that could refer to imported
  // types and also any javadoc references that could refer to imported
  // types (`@link`, `@see`, `@throws`, etc.)
  //
  // No attempt is made to determine whether simple names occur in contexts
  // where they are type names, so there will be false positives. For example,
  // `List` is not identified as unused import below:
  //
  // ```
  // import java.util.List;
  // class List {}
  // ```
  //
  // This is still reasonably effective in practice because type names differ
  // from other kinds of names in casing convention, and simple name
  // clashes between imported and declared types are rare.
  private static class UnusedImportScanner extends ASTVisitor {

    private final Set usedNames = new LinkedHashSet<>();
    private final Multimap usedInJavadoc = HashMultimap.create();

    @Override
    public boolean visit(Javadoc node) {
      node.accept(
          new ASTVisitor(/*visitDocTags=*/ true) {
            @Override
            public boolean visit(TagElement node) {
              recordTag(node);
              return super.visit(node);
            }
          });
      return super.visit(node);
    }

    private void recordTag(TagElement node) {
      if (node.getTagName() == null) {
        return;
      }
      switch (node.getTagName()) {
        case "@exception":
        case "@link":
        case "@linkplain":
        case "@see":
        case "@throws":
        case "@value":
          recordReference(Iterables.getFirst(node.fragments(), null));
          break;
        default:
          break;
      }
    }

    private void recordReference(ASTNode reference) {
      if (reference instanceof SimpleName) {
        recordSimpleName((Name) reference);
      } else if (reference instanceof MemberRef) {
        recordSimpleName(((MemberRef) reference).getQualifier());
      } else if (reference instanceof MethodRef) {
        recordMethodRef((MethodRef) reference);
      }
    }

    private void recordMethodRef(MethodRef methodRef) {
      recordSimpleName(methodRef.getQualifier());
      for (MethodRefParameter parameter : (List) methodRef.parameters()) {
        recordTypeRef(parameter.getType());
      }
    }

    private void recordTypeRef(Type type) {
      if (type instanceof SimpleType) {
        recordSimpleName(((SimpleType) type).getName());
      } else if (type instanceof ArrayType) {
        recordTypeRef(((ArrayType) type).getElementType());
      }
    }

    private void recordSimpleName(Name name) {
      while (name instanceof QualifiedName) {
        name = ((QualifiedName) name).getQualifier();
      }
      if (name instanceof SimpleName) {
        String identifier = ((SimpleName) name).getIdentifier();
        if (Character.isUpperCase(identifier.charAt(0))) {
          usedInJavadoc.put(identifier, name);
        }
      }
    }

    @Override
    public boolean visit(ImportDeclaration node) {
      // skip imports
      return false;
    }

    @Override
    public boolean visit(SimpleName node) {
      usedNames.add(node.getIdentifier().toString());
      return super.visit(node);
    }
  }

  public static String removeUnusedImports(
      final String contents, JavadocOnlyImports javadocOnlyImports) {
    CompilationUnit unit = parse(contents);
    if (unit == null) {
      // error handling is done during formatting
      return contents;
    }
    UnusedImportScanner scanner = new UnusedImportScanner();
    unit.accept(scanner);
    return applyReplacements(
        contents,
        buildReplacements(
            contents, unit, scanner.usedNames, scanner.usedInJavadoc, javadocOnlyImports));
  }

  private static CompilationUnit parse(String source) {
    ASTParser parser = ASTParser.newParser(AST.JLS8);
    parser.setSource(source.toCharArray());
    Map options = JavaCore.getOptions();
    JavaCore.setComplianceOptions(JavaCore.VERSION_1_8, options);
    parser.setCompilerOptions(options);
    CompilationUnit unit = (CompilationUnit) parser.createAST(null);
    if (unit.getMessages().length > 0) {
      // error handling is done during formatting
      return null;
    }
    return unit;
  }

  /** Construct replacements to fix unused imports. */
  private static RangeMap buildReplacements(
      String contents,
      CompilationUnit unit,
      Set usedNames,
      Multimap usedInJavadoc,
      JavadocOnlyImports javadocOnlyImports) {
    RangeMap replacements = TreeRangeMap.create();
    for (ImportDeclaration importTree : (List) unit.imports()) {
      String simpleName = getSimpleName(importTree);
      if (!isUnused(unit, usedNames, usedInJavadoc, javadocOnlyImports, importTree, simpleName)) {
        continue;
      }
      // delete the import
      int endPosition = importTree.getStartPosition() + importTree.getLength();
      endPosition = Math.max(CharMatcher.isNot(' ').indexIn(contents, endPosition), endPosition);
      String sep = System.lineSeparator();
      if (endPosition + sep.length() < contents.length()
          && contents.subSequence(endPosition, endPosition + sep.length()).equals(sep)) {
        endPosition += sep.length();
      }
      replacements.put(Range.closedOpen(importTree.getStartPosition(), endPosition), "");
      // fully qualify any javadoc references with the same simple name as a deleted
      // non-static import
      if (!importTree.isStatic()) {
        for (ASTNode doc : usedInJavadoc.get(simpleName)) {
          String replaceWith = importTree.getName().toString();
          Range range =
              Range.closedOpen(doc.getStartPosition(), doc.getStartPosition() + doc.getLength());
          replacements.put(range, replaceWith);
        }
      }
    }
    return replacements;
  }

  private static String getSimpleName(ImportDeclaration importTree) {
    return importTree.getName() instanceof QualifiedName
        ? ((QualifiedName) importTree.getName()).getName().toString()
        : importTree.getName().toString();
  }

  private static boolean isUnused(
      CompilationUnit unit,
      Set usedNames,
      Multimap usedInJavadoc,
      JavadocOnlyImports javadocOnlyImports,
      ImportDeclaration importTree,
      String simpleName) {
    if (unit.getPackage() != null) {
      String qualifier =
          importTree.isOnDemand()
              ? importTree.getName().toString()
              : importTree.getName() instanceof QualifiedName
                  ? ((QualifiedName) importTree.getName()).getQualifier().toString()
                  : null;
      if (unit.getPackage().getName().toString().equals(qualifier)) {
        return true;
      }
    }
    if (importTree.isOnDemand()) {
      return false;
    }
    if (usedNames.contains(simpleName)) {
      return false;
    }
    if (usedInJavadoc.containsKey(simpleName) && javadocOnlyImports == JavadocOnlyImports.KEEP) {
      return false;
    }
    return true;
  }

  /** Applies the replacements to the given source, and re-format any edited javadoc. */
  private static String applyReplacements(String source, RangeMap replacements) {
    // save non-empty fixed ranges for reformatting after fixes are applied
    RangeSet fixedRanges = TreeRangeSet.create();

    // Apply the fixes in increasing order, adjusting ranges to account for
    // earlier fixes that change the length of the source. The output ranges are
    // needed so we can reformat fixed regions, otherwise the fixes could just
    // be applied in descending order without adjusting offsets.
    StringBuilder sb = new StringBuilder(source);
    int offset = 0;
    for (Map.Entry, String> replacement : replacements.asMapOfRanges().entrySet()) {
      Range range = replacement.getKey();
      String replaceWith = replacement.getValue();
      int start = offset + range.lowerEndpoint();
      int end = offset + range.upperEndpoint();
      sb.replace(start, end, replaceWith);
      if (!replaceWith.isEmpty()) {
        fixedRanges.add(Range.closedOpen(start, end));
      }
      offset += replaceWith.length() - (range.upperEndpoint() - range.lowerEndpoint());
    }
    String result = sb.toString();

    // If there were any non-empty replaced ranges (e.g. javadoc), reformat the fixed regions.
    // We could avoid formatting twice in --fix-imports=also mode, but that is not the default
    // and removing imports won't usually affect javadoc.
    if (!fixedRanges.isEmpty()) {
      try {
        result = new Formatter().formatSource(result, fixedRanges.asRanges());
      } catch (FormatterException e) {
        // javadoc reformatting is best-effort
      }
    }
    return result;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy