
org.sonar.java.checks.PrivateFieldUsedLocallyCheck 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.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.CheckForNull;
import org.sonar.check.Rule;
import org.sonar.java.cfg.CFG;
import org.sonar.java.cfg.LiveVariables;
import org.sonar.java.checks.helpers.QuickFixHelper;
import org.sonar.java.model.ExpressionUtils;
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.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Symbol.TypeSymbol;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
import org.sonar.plugins.java.api.tree.BlockTree;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.SyntaxToken;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.Tree.Kind;
import org.sonar.plugins.java.api.tree.VariableTree;
import static org.sonar.java.checks.helpers.QuickFixHelper.contentForRange;
/**
* Current implementation raises the issue only for the fields used in one method
*/
@Rule(key = "S1450")
public class PrivateFieldUsedLocallyCheck extends IssuableSubscriptionVisitor {
private static final String MESSAGE = "Remove the \"%s\" field and declare it as a local variable in the relevant methods.";
private static final String QUICK_FIX_MESSAGE = "Move this field to the only method where it is used";
@Override
public List nodesToVisit() {
return Collections.singletonList(Kind.CLASS);
}
@Override
public void visitNode(Tree tree) {
TypeSymbol classSymbol = ((ClassTree) tree).symbol();
Set fieldsReadOnAnotherInstance = FieldsReadOnAnotherInstanceVisitor.getFrom(tree);
classSymbol.memberSymbols().stream()
.filter(PrivateFieldUsedLocallyCheck::isPrivateField)
.filter(s -> !isAConstant(s))
.filter(s -> !hasAnnotation(s))
.filter(s -> !s.usages().isEmpty())
.filter(s -> !fieldsReadOnAnotherInstance.contains(s))
.forEach(s -> checkPrivateField(s, classSymbol));
}
private static boolean isAConstant(Symbol s) {
return s.isFinal() && s.isStatic();
}
private static boolean hasAnnotation(Symbol s) {
return !s.metadata().annotations().isEmpty();
}
private void checkPrivateField(Symbol privateFieldSymbol, TypeSymbol classSymbol) {
MethodTree methodWhereUsed = usedInOneMethodOnly(privateFieldSymbol, classSymbol);
if (methodWhereUsed != null && !isLiveInMethodEntry(privateFieldSymbol, methodWhereUsed)) {
VariableTree declaration = (VariableTree) privateFieldSymbol.declaration();
IdentifierTree declarationIdentifier = declaration.simpleName();
String message = String.format(MESSAGE, privateFieldSymbol.name());
QuickFixHelper.newIssue(context)
.forRule(this)
.onTree(declarationIdentifier)
.withMessage(message)
.withQuickFixes(() -> computeQuickFix((Symbol.VariableSymbol) privateFieldSymbol, declaration, methodWhereUsed))
.report();
}
}
private List computeQuickFix(Symbol.VariableSymbol symbol, VariableTree declaration, MethodTree methodWhereUsed) {
if (wouldRelocationClashWithLocalVariables(symbol, methodWhereUsed)) {
return Collections.emptyList();
}
BlockTree block = methodWhereUsed.block();
SyntaxToken openingBrace = block.openBraceToken();
String padding = generateLeftPadding(block);
String declarationMinusModifiers = contentForRange(declaration.type().firstToken(), declaration.endToken(), context);
String newDeclaration = "\n" + padding + declarationMinusModifiers;
return List.of(
JavaQuickFix.newQuickFix(QUICK_FIX_MESSAGE)
.addTextEdits(editUsagesWithThis(symbol))
.addTextEdit(JavaTextEdit.insertAfterTree(openingBrace, newDeclaration))
.addTextEdit(JavaTextEdit.removeTree(declaration))
.build()
);
}
/*
* Compares the field name against local variables and parameters in the method to ensure that moving the
* field declaration to the method would not create any clash.
*/
private static boolean wouldRelocationClashWithLocalVariables(Symbol.VariableSymbol symbol, MethodTree method) {
LocalVariableCollector collector = new LocalVariableCollector();
method.accept(collector);
if (collector.variables.isEmpty()) {
return false;
}
return collector.variables
.stream()
.anyMatch(variable -> variable.symbol().name().equals(symbol.name()));
}
private static String generateLeftPadding(BlockTree block) {
int spacesOnTheLeft = Math.max(0, block.body().get(0).firstToken().range().start().column() - 1);
return " ".repeat(spacesOnTheLeft);
}
/**
* Returns edits to transform all usages in the form of this.myVariable to myVariable.
*/
private static List editUsagesWithThis(Symbol symbol) {
return symbol.usages().stream()
.map(Tree::parent)
.filter(parent -> parent.is(Kind.MEMBER_SELECT))
.map(MemberSelectExpressionTree.class::cast)
.filter(memberSelect -> ExpressionUtils.isThis(memberSelect.expression()))
.map(memberSelect -> JavaTextEdit.removeBetweenTree(memberSelect.expression(), memberSelect.operatorToken()))
.toList();
}
private static boolean isLiveInMethodEntry(Symbol privateFieldSymbol, MethodTree methodTree) {
CFG cfg = (CFG) methodTree.cfg();
LiveVariables liveVariables = LiveVariables.analyzeWithFields(cfg);
return liveVariables.getIn(cfg.entryBlock()).contains(privateFieldSymbol);
}
private static boolean isPrivateField(Symbol memberSymbol) {
return memberSymbol.isPrivate() && memberSymbol.isVariableSymbol();
}
/**
* If private field used in several methods then returns null, otherwise returns the method where it's used
*/
@CheckForNull
private static MethodTree usedInOneMethodOnly(Symbol privateFieldSymbol, TypeSymbol classSymbol) {
MethodTree method = null;
for (IdentifierTree usageIdentifier : privateFieldSymbol.usages()) {
MethodTree enclosingMethod = (MethodTree) ExpressionUtils.getEnclosingTree(usageIdentifier, Kind.METHOD);
if (enclosingMethod == null
|| !enclosingMethod.symbol().owner().equals(classSymbol)
|| (method != null && !method.equals(enclosingMethod))) {
return null;
} else {
method = enclosingMethod;
}
}
return method;
}
private static class FieldsReadOnAnotherInstanceVisitor extends BaseTreeVisitor {
private final Set fieldsReadOnAnotherInstance = new HashSet<>();
static Set getFrom(Tree classTree) {
FieldsReadOnAnotherInstanceVisitor fieldsReadOnAnotherInstanceVisitor = new FieldsReadOnAnotherInstanceVisitor();
fieldsReadOnAnotherInstanceVisitor.scan(classTree);
return fieldsReadOnAnotherInstanceVisitor.fieldsReadOnAnotherInstance;
}
@Override
public void visitMemberSelectExpression(MemberSelectExpressionTree tree) {
Symbol symbol = tree.identifier().symbol();
if (isField(symbol)) {
if (tree.expression().is(Kind.IDENTIFIER)) {
if (!ExpressionUtils.isThis(tree.expression())) {
fieldsReadOnAnotherInstance.add(symbol);
}
} else {
fieldsReadOnAnotherInstance.add(symbol);
}
}
super.visitMemberSelectExpression(tree);
}
}
private static class LocalVariableCollector extends BaseTreeVisitor {
private final Set variables = new HashSet<>();
@Override
public void visitVariable(VariableTree tree) {
variables.add(tree);
}
}
public static boolean isField(Symbol symbol) {
return symbol.isVariableSymbol()
&& !symbol.isStatic()
&& !symbol.owner().isMethodSymbol();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy