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

org.sonar.java.checks.UselessImportCheck Maven / Gradle / Ivy

There is a newer version: 8.9.0.37768
Show newest version
/*
 * SonarQube Java
 * Copyright (C) 2012-2024 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the Sonar Source-Available License for more details.
 *
 * You should have received a copy of the Sonar Source-Available License
 * along with this program; if not, see https://sonarsource.com/license/ssal/
 */
package org.sonar.java.checks;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.sonar.check.Rule;
import org.sonar.java.checks.helpers.ExpressionsHelper;
import org.sonar.java.checks.helpers.QuickFixHelper;
import org.sonar.java.model.JProblem;
import org.sonar.java.model.JWarning;
import org.sonar.java.model.JavaTree.CompilationUnitTreeImpl;
import org.sonar.java.reporting.AnalyzerMessage;
import org.sonar.java.reporting.JavaQuickFix;
import org.sonar.java.reporting.JavaTextEdit;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.CompilationUnitTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.ImportTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.PackageDeclarationTree;
import org.sonar.plugins.java.api.tree.SyntaxTrivia;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonarsource.analyzer.commons.annotations.DeprecatedRuleKey;

@DeprecatedRuleKey(ruleKey = "UselessImportCheck", repositoryKey = "squid")
@Rule(key = "S1128")
public class UselessImportCheck extends IssuableSubscriptionVisitor {

  private static final Pattern COMPILER_WARNING = Pattern.compile("The import ([$\\w]+(\\.[$\\w]+)*+) is never used");
  private static final Pattern NON_WORDS_CHARACTERS = Pattern.compile("\\W+");
  private static final Pattern JAVADOC_REFERENCE = Pattern.compile("\\{@link[^\\}]*\\}|(@see|@throws)[^\n]*\n");

  private String currentPackage = "";
  private final List imports = new ArrayList<>();
  private final Map importsNames = new HashMap<>();
  private final Set duplicatedImports = new HashSet<>();
  private final Set usedInJavaDoc = new HashSet<>();

  @Override
  public List nodesToVisit() {
    return Arrays.asList(Tree.Kind.TRIVIA, Tree.Kind.COMPILATION_UNIT, Tree.Kind.PACKAGE, Tree.Kind.IMPORT);
  }

  @Override
  public void visitNode(Tree tree) {
    if (tree.is(Tree.Kind.COMPILATION_UNIT)) {
      ((CompilationUnitTree) tree).imports()
        .stream()
        .filter(importTree -> importTree.is(Tree.Kind.IMPORT))
        .map(ImportTree.class::cast)
        .forEach(imports::add);
      importsNames.clear();
      duplicatedImports.clear();
      usedInJavaDoc.clear();
      currentPackage = "";
    } else if (tree.is(Tree.Kind.PACKAGE)) {
      currentPackage = ExpressionsHelper.concatenate(((PackageDeclarationTree) tree).packageName());
    } else {
      handleImportTree((ImportTree) tree);
    }
  }

  @Override
  public void leaveNode(Tree tree) {
    if (tree.is(Tree.Kind.COMPILATION_UNIT)) {
      handleWarnings(((CompilationUnitTreeImpl) tree).warnings(JProblem.Type.UNUSED_IMPORT));
      imports.clear();
    }
  }

  private void handleWarnings(List warnings) {
    for (JWarning warning : warnings) {
      Matcher matcher = COMPILER_WARNING.matcher(warning.message());
      Optional fqn = matcher.find() ? Optional.of(matcher.group(1)) : Optional.empty();
      fqn.ifPresent(importName -> {
        if (!usedInJavaDoc.contains(importName) && !importName.startsWith("java.lang")) {
          String message;
          if (duplicatedImports.contains(importName)) {
            message = "Remove this duplicated import.";
          } else if (isImportFromSamePackage(importName, warning.syntaxTree())) {
            message = "Remove this unnecessary import: same package classes are always implicitly imported.";
          } else {
            message = "Remove this unused import '" + importName + "'.";
          }
          ImportTree reportTree = (ImportTree) warning.syntaxTree();
          QuickFixHelper.newIssue(context)
            .forRule(this)
            .onTree(reportTree.qualifiedIdentifier())
            .withMessage(message)
            .withQuickFix(() -> quickFix(reportTree, imports))
            .report();
        }
      });
    }
  }

  private boolean isImportFromSamePackage(String importName, Tree tree) {
    // ECJ warning message does not contain the ".*" in case of star import, we have to find out if we are in this case.
    // Defensive programming, the syntax tree of the warning should always be an ImportTree.
    if (tree.is(Tree.Kind.IMPORT)) {
      Tree qualifiedIdentifier = ((ImportTree) tree).qualifiedIdentifier();
      // Defensive programming, the qualifiedIdentifier should always be a MemberSelectTree.
      if (qualifiedIdentifier.is(Tree.Kind.MEMBER_SELECT) &&
        "*".equals(((MemberSelectExpressionTree) qualifiedIdentifier).identifier().name())) {
        return importName.equals(currentPackage);
      }
    }

    return importName.substring(0, importName.lastIndexOf(".")).equals(currentPackage);
  }

  private void handleImportTree(ImportTree importTree) {
    String importName = ExpressionsHelper.concatenate(((ExpressionTree) importTree.qualifiedIdentifier()));
    if (importsNames.containsKey(importName)) {
      duplicatedImports.add(importName);
    } else {
      importsNames.put(importName, extractLastClassName(importName));
    }
    if (isJavaLangImport(importName)) {
      QuickFixHelper.newIssue(context)
        .forRule(this)
        .onTree(importTree.qualifiedIdentifier())
        .withMessage("Remove this unnecessary import: java.lang classes are always implicitly imported.")
        .withQuickFix(() -> quickFix(importTree, imports))
        .report();
    }
  }

  private static String extractLastClassName(String reference) {
    int lastIndexOfDot = reference.lastIndexOf('.');
    return lastIndexOfDot == -1 ? reference : reference.substring(lastIndexOfDot + 1);
  }

  private static boolean isJavaLangImport(String reference) {
    return reference.startsWith("java.lang.") && reference.indexOf('.', "java.lang.".length()) == -1;
  }

  @Override
  public void visitTrivia(SyntaxTrivia syntaxTrivia) {
    String comment = syntaxTrivia.comment();
    if (!comment.startsWith("/**")) {
      return;
    }
    Matcher matcher = JAVADOC_REFERENCE.matcher(comment);
    while (matcher.find()) {
      String line = matcher.group(0);
      Set words = NON_WORDS_CHARACTERS.splitAsStream(line)
        .filter(w -> !w.isEmpty())
        .collect(Collectors.toSet());

      if (!words.isEmpty()) {
        importsNames.forEach((fullyQualifiedName, name) -> {
          if (words.contains(name)) {
            usedInJavaDoc.add(fullyQualifiedName);
          }
        });
      }
    }
  }

  private static JavaQuickFix quickFix(ImportTree importTree, List imports) {
    int indexOfImport = imports.indexOf(importTree);
    boolean isLastImport = indexOfImport == imports.size() - 1;
    JavaQuickFix.Builder quickFix = JavaQuickFix.newQuickFix("Remove the %simport", importTree.isStatic() ? "static " : "");
    if (imports.size() == 1) {
      // single import not used...
      quickFix.addTextEdit(JavaTextEdit.removeTree(importTree));
    } else if (!isLastImport) {
      ImportTree nextImport = imports.get(indexOfImport + 1);
      quickFix.addTextEdit(JavaTextEdit.removeTextSpan(AnalyzerMessage.textSpanBetween(importTree, true, nextImport, false)));
    } else {
      // last import
      ImportTree previousImport = imports.get(indexOfImport - 1);
      quickFix.addTextEdit(JavaTextEdit.removeTextSpan(AnalyzerMessage.textSpanBetween(previousImport, false, importTree, true)));
    }
    return quickFix.build();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy