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

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

/*
 * 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.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.sonar.check.Rule;
import org.sonar.java.model.LineUtils;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
import org.sonar.plugins.java.api.tree.LambdaExpressionTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.NewClassTree;
import org.sonar.plugins.java.api.tree.ReturnStatementTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.VariableTree;

@Rule(key = "S1845")
public class MembersDifferOnlyByCapitalizationCheck extends IssuableSubscriptionVisitor {

  private static final String ISSUE_MESSAGE = "Rename %s \"%s\" to prevent any misunderstanding/clash with %s \"%s\"%s.";

  @Override
  public List nodesToVisit() {
    return Arrays.asList(Tree.Kind.CLASS, Tree.Kind.INTERFACE, Tree.Kind.ENUM, Tree.Kind.RECORD);
  }

  @Override
  public void visitNode(Tree tree) {
    ClassTree classTree = (ClassTree) tree;
    List allMembers = retrieveMembers(classTree.symbol());
    Map> membersByName = allMembers.stream().collect(Collectors.groupingBy(Symbol::name));
    for (Tree member : classTree.members()) {
      if (member.is(Tree.Kind.METHOD)) {
        MethodTree methodTree = (MethodTree) member;
        checkForIssue(methodTree.symbol(), methodTree.simpleName(), membersByName);
      } else if (member.is(Tree.Kind.VARIABLE)) {
        VariableTree variableTree = (VariableTree) member;
        checkForIssue(variableTree.symbol(), variableTree.simpleName(), membersByName);
      }
    }
  }

  private void checkForIssue(Symbol symbol, IdentifierTree reportTree, Map> membersByName) {
    String name = symbol.name();
    for (Map.Entry> knownMemberName : membersByName.entrySet()) {
      if (name.equalsIgnoreCase(knownMemberName.getKey())) {
        Optional firstConflict = knownMemberName.getValue().stream()
          .filter(knownMemberSymbol -> !symbol.equals(knownMemberSymbol) && isValidIssueLocation(symbol, knownMemberSymbol) && isInvalidMember(symbol, knownMemberSymbol))
          .findFirst();

        if (firstConflict.isPresent()) {
          Symbol conflictingSymbol = firstConflict.get();
          reportIssue(reportTree, issueMessage(symbol, knownMemberName, conflictingSymbol), declarationTree(conflictingSymbol), null);
          // We report one issue per problematic member, with a single secondary location.
          break;
        }
      }
    }
  }

  private static String issueMessage(Symbol symbol, Map.Entry> knownMemberName, Symbol conflictingSymbol) {
    return String.format(ISSUE_MESSAGE,
      getSymbolKindName(symbol),
      symbol.name(),
      getSymbolKindName(conflictingSymbol),
      knownMemberName.getKey(),
      getDefinitionPlace(symbol, conflictingSymbol));
  }

  private static List declarationTree(Symbol symbol) {
    Tree reportTree = null;
    if (symbol.isMethodSymbol()) {
      MethodTree declaration = ((Symbol.MethodSymbol) symbol).declaration();
      if (declaration != null) {
        reportTree = declaration.simpleName();
      }
    } else if (symbol.isVariableSymbol()) {
      VariableTree declaration = ((Symbol.VariableSymbol) symbol).declaration();
      if (declaration != null) {
        reportTree = declaration.simpleName();
      }
    }
    if (reportTree == null) {
      return Collections.emptyList();
    }
    return Collections.singletonList(new JavaFileScannerContext.Location("Conflicting identifier", reportTree));
  }

  private static boolean isOverriding(Symbol symbol) {
    if (symbol.isMethodSymbol()) {
      MethodTree methodDeclaration = (MethodTree) symbol.declaration();
      return methodDeclaration != null && Boolean.TRUE.equals(methodDeclaration.isOverriding());
    }
    return false;
  }

  private static boolean isInvalidMember(Symbol currentMember, Symbol knownMember) {
    if (!isOverriding(currentMember)) {
      return differentSymbolKinds(currentMember, knownMember) ? invalidMethodAndVariable(currentMember, knownMember) : !sameName(currentMember, knownMember);
    }
    return false;
  }

  private static boolean invalidMethodAndVariable(Symbol currentMember, Symbol knownMember) {
    if (!sameVisibilityNotPrivate(currentMember, knownMember)) {
      return false;
    }
    Symbol methodSymbol = currentMember.isMethodSymbol() ? currentMember : knownMember;
    Symbol variableSymbol = methodSymbol == currentMember ? knownMember : currentMember;
    return !methodReturningVariableWithSameName(methodSymbol, variableSymbol)
      && !isBuilderPattern(methodSymbol, variableSymbol);
  }

  private static boolean isBuilderPattern(Symbol methodSymbol, Symbol variableSymbol) {
    return methodSymbol.owner().name().endsWith("Builder") && sameName(methodSymbol, variableSymbol);
  }

  private static boolean methodReturningVariableWithSameName(Symbol methodSymbol, Symbol variableSymbol) {
    if (!sameName(variableSymbol, methodSymbol)) {
      return false;
    }
    Tree declaration = methodSymbol.declaration();
    if (declaration != null) {
      ReturnVisitor returnVisitor = new ReturnVisitor(variableSymbol);
      declaration.accept(returnVisitor);
      return returnVisitor.singleReturnWithVariableSymbol();
    }
    return false;
  }

  private static boolean isValidIssueLocation(Symbol currentMember, Symbol knownMember) {
    return !sameOwner(currentMember, knownMember) || isOverriding(knownMember) || getDeclarationLine(currentMember) > getDeclarationLine(knownMember);
  }

  private static boolean sameVisibilityNotPrivate(Symbol s1, Symbol s2) {
    return bothPublic(s1, s2) || bothProtected(s1, s2) || bothPackageVisibility(s1, s2);
  }

  private static boolean bothPackageVisibility(Symbol s1, Symbol s2) {
    return s1.isPackageVisibility() && s2.isPackageVisibility();
  }

  private static boolean bothProtected(Symbol s1, Symbol s2) {
    return s1.isProtected() && s2.isProtected();
  }

  private static boolean bothPublic(Symbol s1, Symbol s2) {
    return s1.isPublic() && s2.isPublic();
  }

  private static boolean sameOwner(Symbol currentMember, Symbol knownMember) {
    return currentMember.owner().equals(knownMember.owner());
  }

  private static boolean sameName(Symbol currentMember, Symbol knownMember) {
    return currentMember.name().equals(knownMember.name());
  }

  private static boolean differentSymbolKinds(Symbol s1, Symbol s2) {
    return variableAndMethod(s1, s2) || variableAndMethod(s2, s1);
  }

  private static boolean variableAndMethod(Symbol s1, Symbol s2) {
    return s1.isVariableSymbol() && s2.isMethodSymbol();
  }

  private static String getDefinitionPlace(Symbol symbol, Symbol knownMemberSymbol) {
    if (sameOwner(symbol, knownMemberSymbol)) {
      return "";
    }
    Symbol owner = knownMemberSymbol.owner();
    return " defined in " + (owner.isInterface() ? "interface" : "superclass") + " \"" + owner.type().fullyQualifiedName() + "\"";
  }

  private static int getDeclarationLine(Symbol symbol) {
    if (symbol.declaration() == null) {
      return -1;
    }
    if (symbol.isVariableSymbol()) {
      return LineUtils.startLine(((Symbol.VariableSymbol) symbol).declaration().simpleName().identifierToken());
    }
    return LineUtils.startLine(((Symbol.MethodSymbol) symbol).declaration().simpleName().identifierToken());
  }

  private static String getSymbolKindName(Symbol symbol) {
    return symbol.isMethodSymbol() ? "method" : "field";
  }

  private static List retrieveMembers(Symbol.TypeSymbol classSymbol) {
    List results = new LinkedList<>();
    results.addAll(extractMembers(classSymbol, false));

    for (Type parentInterface : classSymbol.interfaces()) {
      results.addAll(extractMembers(parentInterface.symbol(), true));
    }
    Type superClass = classSymbol.superClass();
    if (superClass != null) {
      results.addAll(extractMembers(superClass.symbol(), true));
    }

    return results;
  }

  private static List extractMembers(Symbol.TypeSymbol classSymbol, boolean ignorePrivate) {
    List results = new LinkedList<>();
    for (Symbol symbol : classSymbol.memberSymbols()) {
      if ((isVariableToExtract(symbol) || isMethodToExtract(symbol)) && !(symbol.isPrivate() && ignorePrivate)) {
        results.add(symbol);
      }
    }
    return results;
  }

  private static boolean isVariableToExtract(Symbol symbol) {
    String name = symbol.name();
    return !symbol.isEnum() && symbol.isVariableSymbol() && !"this".equals(name) && !"super".equals(name);
  }

  private static boolean isMethodToExtract(Symbol symbol) {
    return symbol.isMethodSymbol() && !"".equals(symbol.name());
  }

  private static class ReturnVisitor extends BaseTreeVisitor {

    private final Symbol variableSymbol;
    private boolean returnsVariable;
    private int returnCount;

    ReturnVisitor(Symbol variableSymbol) {
      this.variableSymbol = variableSymbol;
    }

    @Override
    public void visitReturnStatement(ReturnStatementTree tree) {
      returnCount++;
      ExpressionTree returnExpression = tree.expression();
      if (returnExpression != null && returnExpression.is(Tree.Kind.IDENTIFIER)) {
        returnsVariable = ((IdentifierTree) returnExpression).symbol().equals(variableSymbol);
      }
    }

    @Override
    public void visitLambdaExpression(LambdaExpressionTree lambdaExpressionTree) {
      // not interested in returns in lambda bodies
    }

    @Override
    public void visitNewClass(NewClassTree tree) {
      // not interested in anonymous class bodies
    }

    boolean singleReturnWithVariableSymbol() {
      return returnCount == 1 && returnsVariable;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy