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 static java.lang.Math.max;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.base.CharMatcher;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
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 com.google.googlejavaformat.Newlines;
import com.sun.source.doctree.DocCommentTree;
import com.sun.source.doctree.ReferenceTree;
import com.sun.source.tree.CaseTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.DocTreePath;
import com.sun.source.util.DocTreePathScanner;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.parser.JavacParser;
import com.sun.tools.javac.parser.ParserFactory;
import com.sun.tools.javac.tree.DCTree;
import com.sun.tools.javac.tree.DCTree.DCReference;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.JCTree.JCImport;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Log;
import com.sun.tools.javac.util.Options;
import java.io.IOError;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.DiagnosticListener;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardLocation;

/**
 * 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 {

  // 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 TreePathScanner {

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

    private UnusedImportScanner(JavacTrees trees) {
      this.trees = trees;
      docTreeSymbolScanner = new DocTreeScanner();
    }

    /** Skip the imports themselves when checking for usage. */
    @Override
    public Void visitImport(ImportTree importTree, Void usedSymbols) {
      return null;
    }

    @Override
    public Void visitIdentifier(IdentifierTree tree, Void unused) {
      if (tree == null) {
        return null;
      }
      usedNames.add(tree.getName().toString());
      return null;
    }

    // TODO(cushon): remove this override when pattern matching in switch is no longer a preview
    // feature, and TreePathScanner visits CaseTree#getLabels instead of CaseTree#getExpressions
    @SuppressWarnings("unchecked") // reflection
    @Override
    public Void visitCase(CaseTree tree, Void unused) {
      if (CASE_TREE_GET_LABELS != null) {
        try {
          scan((List) CASE_TREE_GET_LABELS.invoke(tree), null);
        } catch (ReflectiveOperationException e) {
          throw new LinkageError(e.getMessage(), e);
        }
      }
      return super.visitCase(tree, null);
    }

    private static final Method CASE_TREE_GET_LABELS = caseTreeGetLabels();

    private static Method caseTreeGetLabels() {
      try {
        return CaseTree.class.getMethod("getLabels");
      } catch (NoSuchMethodException e) {
        return null;
      }
    }

    @Override
    public Void scan(Tree tree, Void unused) {
      if (tree == null) {
        return null;
      }
      scanJavadoc();
      return super.scan(tree, unused);
    }

    private void scanJavadoc() {
      if (getCurrentPath() == null) {
        return;
      }
      DocCommentTree commentTree = trees.getDocCommentTree(getCurrentPath());
      if (commentTree == null) {
        return;
      }
      docTreeSymbolScanner.scan(new DocTreePath(getCurrentPath(), commentTree), null);
    }

    // scan javadoc comments, checking for references to imported types
    class DocTreeScanner extends DocTreePathScanner {
      @Override
      public Void visitIdentifier(com.sun.source.doctree.IdentifierTree node, Void aVoid) {
        return null;
      }

      @Override
      public Void visitReference(ReferenceTree referenceTree, Void unused) {
        DCReference reference = (DCReference) referenceTree;
        long basePos =
            reference
                .pos((DCTree.DCDocComment) getCurrentPath().getDocComment())
                .getStartPosition();
        // the position of trees inside the reference node aren't stored, but the qualifier's
        // start position is the beginning of the reference node
        if (reference.qualifierExpression != null) {
          new ReferenceScanner(basePos).scan(reference.qualifierExpression, null);
        }
        // Record uses inside method parameters. The javadoc tool doesn't use these, but
        // IntelliJ does.
        if (reference.paramTypes != null) {
          for (JCTree param : reference.paramTypes) {
            // TODO(cushon): get start positions for the parameters
            new ReferenceScanner(-1).scan(param, null);
          }
        }
        return null;
      }

      // scans the qualifier and parameters of a javadoc reference for possible type names
      private class ReferenceScanner extends TreeScanner {
        private final long basePos;

        public ReferenceScanner(long basePos) {
          this.basePos = basePos;
        }

        @Override
        public Void visitIdentifier(IdentifierTree node, Void aVoid) {
          usedInJavadoc.put(
              node.getName().toString(),
              basePos != -1
                  ? Range.closedOpen((int) basePos, (int) basePos + node.getName().length())
                  : null);
          return super.visitIdentifier(node, aVoid);
        }
      }
    }
  }

  public static String removeUnusedImports(final String contents) throws FormatterException {
    Context context = new Context();
    JCCompilationUnit unit = parse(context, contents);
    if (unit == null) {
      // error handling is done during formatting
      return contents;
    }
    UnusedImportScanner scanner = new UnusedImportScanner(JavacTrees.instance(context));
    scanner.scan(unit, null);
    return applyReplacements(
        contents, buildReplacements(contents, unit, scanner.usedNames, scanner.usedInJavadoc));
  }

  private static JCCompilationUnit parse(Context context, String javaInput)
      throws FormatterException {
    DiagnosticCollector diagnostics = new DiagnosticCollector<>();
    context.put(DiagnosticListener.class, diagnostics);
    Options.instance(context).put("--enable-preview", "true");
    Options.instance(context).put("allowStringFolding", "false");
    JCCompilationUnit unit;
    JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
    try {
      fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
    } catch (IOException e) {
      // impossible
      throw new IOError(e);
    }
    SimpleJavaFileObject source =
        new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
          @Override
          public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
            return javaInput;
          }
        };
    Log.instance(context).useSource(source);
    ParserFactory parserFactory = ParserFactory.instance(context);
    JavacParser parser =
        parserFactory.newParser(
            javaInput,
            /* keepDocComments= */ true,
            /* keepEndPos= */ true,
            /* keepLineMap= */ true);
    unit = parser.parseCompilationUnit();
    unit.sourcefile = source;
    Iterable> errorDiagnostics =
        Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic);
    if (!Iterables.isEmpty(errorDiagnostics)) {
      // error handling is done during formatting
      throw FormatterException.fromJavacDiagnostics(errorDiagnostics);
    }
    return unit;
  }

  /** Construct replacements to fix unused imports. */
  private static RangeMap buildReplacements(
      String contents,
      JCCompilationUnit unit,
      Set usedNames,
      Multimap> usedInJavadoc) {
    RangeMap replacements = TreeRangeMap.create();
    for (JCTree importTree : unit.getImports()) {
      String simpleName = getSimpleName(importTree);
      if (!isUnused(unit, usedNames, usedInJavadoc, importTree, simpleName)) {
        continue;
      }
      // delete the import
      int endPosition = importTree.getEndPosition(unit.endPositions);
      endPosition = max(CharMatcher.isNot(' ').indexIn(contents, endPosition), endPosition);
      String sep = Newlines.guessLineSeparator(contents);
      if (endPosition + sep.length() < contents.length()
          && contents.subSequence(endPosition, endPosition + sep.length()).toString().equals(sep)) {
        endPosition += sep.length();
      }
      replacements.put(Range.closedOpen(importTree.getStartPosition(), endPosition), "");
    }
    return replacements;
  }

  private static String getSimpleName(JCTree importTree) {
    return getQualifiedIdentifier(importTree).getIdentifier().toString();
  }

  private static boolean isUnused(
      JCCompilationUnit unit,
      Set usedNames,
      Multimap> usedInJavadoc,
      JCTree importTree,
      String simpleName) {
    JCFieldAccess qualifiedIdentifier = getQualifiedIdentifier(importTree);
    String qualifier = qualifiedIdentifier.getExpression().toString();
    if (qualifier.equals("java.lang")) {
      return true;
    }
    if (unit.getPackageName() != null && unit.getPackageName().toString().equals(qualifier)) {
      return true;
    }
    if (qualifiedIdentifier.getIdentifier().contentEquals("*")) {
      return false;
    }

    if (usedNames.contains(simpleName)) {
      return false;
    }
    if (usedInJavadoc.containsKey(simpleName)) {
      return false;
    }
    return true;
  }

  private static JCFieldAccess getQualifiedIdentifier(JCTree importTree) {
    // Use reflection because the return type is JCTree in some versions and JCFieldAccess in others
    try {
      return (JCFieldAccess) JCImport.class.getMethod("getQualifiedIdentifier").invoke(importTree);
    } catch (ReflectiveOperationException e) {
      throw new LinkageError(e.getMessage(), e);
    }
  }

  /** 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());
    }
    return sb.toString();
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy