Hi,
I am trying to program a custom rule:
- if a class named
...State
is embedded into a class by the @Autowired
annotation
- and a getter of this class is invoked
- then this getter invocation must be embedded in a
Objects.requireNonNull()
.
So, this shows a valid and an invalid invocation:
public class MyClass {
@Autowired
private MyState myState;
public void method() {
String value1 = Objects.requireNonNull(myState.getValue1()); // valid
String value2 = myState.getvalue2(); // invalid
}
}
I started with a rule extending IssuableSubscriptionVisitor
. As nodes to visit, I respond with List.of(Tree.Kind.CLASS)
Then I overwrote visitNode(tree)
. First I invoke a BasicTreeVisitor and collect all fields which are annotated by @Autowired
, and where the class name ends with State
.
Then I wanted to add a further BasicTreeVisitor which overwrites visitMethodInvocation. This one should check whether the method was invoked for a field which had been collected in the first BasicTreeVisitor. Then this second BasicTreeVisitor should check whether a sort of parent of the current MethodInvocationTree is Objects.requireNonNull
. How can I navigate to such a parent.
I currently use <sonarplugin.apiVersion>10.6.0.2114</sonarplugin.apiVersion>
Here are the valid scenarios:
public class MyClass {
@Autowired
private MyState myState;
@Autowired
private MyStatus myStatus;
public void method() {
String value1 = Objects.requireNonNull(myState.getValue1()); // Compliant because in Objects.requireNonNull
String value2 = myStatus.getValue2(); // Compliant because it's not a state class
Class someOtherClass = new SomeOtherClass();
String value3 = someOtherClass.getValue3(); // Compliant because it is not @Autowired
String value4 = getValue4(); // Compliant because it is a local method
}
private String getValue4() {
return "4";
}
}
and here the invalid:
public class MyClass {
@Autowired
private MyState myState;
public void method() {
String value1 = myState.getValue1(); // Noncompliant
}
}
And here is my attempt for the rule. I marked with a comment where I got stuck:
import com.eon.test.automation.sonar.tree.MethodTreeService;
import com.eon.test.automation.sonar.tree.VariableTreeService;
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.VariableTree;
import java.util.Collections;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
@Rule(key = "TA0035")
public class StateGettersNonNull extends IssuableSubscriptionVisitor {
@Override
public List<Tree.Kind> nodesToVisit() {
return List.of(Tree.Kind.CLASS);
}
@Override
public void visitNode(Tree tree) {
AutowiredsCollector autowiredsCollector = new AutowiredsCollector();
tree.accept(autowiredsCollector);
SortedMap<String, VariableTree> foundAutowireds = autowiredsCollector.getFoundStateVariables();
}
private static class AutowiredsCollector extends BaseTreeVisitor {
private final SortedMap<String, VariableTree> foundStateVariables = new TreeMap<>();
@Override
public void visitVariable(VariableTree tree) {
String typeName = VariableTreeService.getType(tree);
if (typeName.endsWith("State")) {
List<String> annotations = VariableTreeService.determineAnnotations(tree);
if (annotations.contains("Autowired")) {
String fieldName = VariableTreeService.determineName(tree);
foundStateVariables.put(fieldName, tree);
}
}
super.visitVariable(tree);
}
public SortedMap<String, VariableTree> getFoundStateVariables() {
SortedMap<String, VariableTree> result = Collections.unmodifiableSortedMap(foundStateVariables);
return result;
}
}
private static class MethodInvocationChecker extends BaseTreeVisitor {
private final SortedMap<String, VariableTree> foundStateVariables;
MethodInvocationChecker(SortedMap<String, VariableTree> foundStateVariables) {
this.foundStateVariables = Collections.unmodifiableSortedMap(foundStateVariables);
}
@Override
public void visitMethodInvocation(MethodInvocationTree tree) {
if (MethodTreeService.isGetter(tree)) {
String fieldName = MethodTreeService.determineCallingField(tree);
if (foundStateVariables.containsKey(fieldName)) {
// from here I want to check whether the MethodInvocationTree I currently look at is
// embedded in an "Objects.requireNonNull" or not
}
}
super.visitMethodInvocation(tree);
}
}
}
Hello there, sorry for the late reply, I hope this still helps!
You could try using the MethodMatchers
API:
private static final MethodMatchers REQUIRE_NON_NULL = MethodMatchers.create()
.ofTypes("java.util.Objects")
.names("requireNonNull")
.addParametersMatcher("java.lang.Object")
.build();
You can define a matcher like this, and the case you are looking for is when your MethodInvocationTree
has as direct parent another method invocation, and you want to check if it matches with the specified signature.
So I would update your code like so:
@Override
public void visitMethodInvocation(MethodInvocationTree tree) {
if (MethodTreeService.isGetter(tree)) {
String fieldName = MethodTreeService.determineCallingField(tree);
if (foundStateVariables.containsKey(fieldName)) {
MethodInvocationTreeImpl mitImpl = (MethodInvocationTreeImpl) tree;
if (mitImpl.parent() instanceof MethodInvocationTreeImpl parentMit && REQUIRE_NON_NULL.matches(parentMit)) {
// Here you know that the method invocation was inside a Objects.requireNonNull check
}
}
}
super.visitMethodInvocation(tree);
}
Hi Leonardo, thanks for your hint. It did not work out immediately, but it eventually pointed me towards a solution.
First, I created a serializable class Invoker
which has 2 fields named “field” and “method”.
Then, I extended my MethodTreeService class by this method which recursively determines an invocation chain upwards:
public static List<Invoker> invocationChain(MethodInvocationTree methodInvocationTree) {
List<Invoker> result = new ArrayList<>();
result = invocationChain(result, methodInvocationTree);
return result;
}
private static List<Invoker> invocationChain(List<Invoker> source, MethodInvocationTree methodInvocationTree) {
List<Invoker> result = source;
Tree parent = methodInvocationTree.parent();
if (parent instanceof ArgumentListTreeImpl atil) {
result = invocationChain(result, atil);
}
return result;
}
private static List<Invoker> invocationChain(List<Invoker> source, ArgumentListTreeImpl atil) {
List<Invoker> result = source;
Tree parentTree = atil.parent();
if (parentTree instanceof MethodInvocationTree mit) {
ExpressionTree methodSelect = mit.methodSelect();
Tree.Kind methodSelectKind = methodSelect.kind();
if (Tree.Kind.MEMBER_SELECT.equals(methodSelectKind)) {
MemberSelectExpressionTree memberSelectExpressionTree = (MemberSelectExpressionTree) methodSelect;
IdentifierTree identifierTree = memberSelectExpressionTree.identifier();
String methodName = identifierTree.name();
ExpressionTree expressionTree = memberSelectExpressionTree.expression();
Tree.Kind expressionKind = expressionTree.kind();
String fieldName;
if (Tree.Kind.IDENTIFIER.equals(expressionKind)) {
IdentifierTree identifierTree2 = (IdentifierTree) expressionTree;
fieldName = identifierTree2.name();
} else {
fieldName = null;
}
Invoker invoker = new Invoker(fieldName, methodName);
result.add(invoker);
Tree grandParent = memberSelectExpressionTree.parent();
if (grandParent != null) {
Tree.Kind grandParentKind = grandParent.kind();
if (Tree.Kind.METHOD_INVOCATION.equals(grandParentKind)) {
MethodInvocationTree grandParentMit = (MethodInvocationTree) grandParent;
result = invocationChain(result, grandParentMit);
}
}
}
}
return result;
}
So, if Objects.requireNonNull(someOtherService.doSomething(myState.getValue1()))
would be checked for the myState.getValue1()
, the result would be a List of Invokers. The first one will contain “someOtherSerivce.doSomethingElse”, the second “Objects.requireNonNull”.
Then, in my rule I ask that service for the invocation chain and take a look at the first element. If it is “Objects.requireNonNull”, everything is fine.
1 Like
unfortunately, when I perform some checks within my SonarQube server, I face this isue:
Execution default-cli of goal org.sonarsource.scanner.maven:sonar-maven-plugin:3.9.1.2184:sonar failed: A required class was missing while executing org.sonarsource.scanner.maven:sonar-maven-plugin:3.9.1.2184:sonar: org/sonar/java/ast/parser/ArgumentListTreeImpl
[ERROR] -----------------------------------------------------
[ERROR] realm = plugin>org.codehaus.mojo:sonar-maven-plugin:3.9.1.2184
[ERROR] strategy = org.codehaus.plexus.classworlds.strategy.SelfFirstStrategy
[ERROR] urls[0] = file:/C:/Users/C33016/.m2/repository/org/sonarsource/scanner/maven/sonar-maven-plugin/3.9.1.2184/sonar-maven-plugin-3.9.1.2184.jar
[ERROR] urls[1] = file:/C:/Users/C33016/.m2/repository/org/sonatype/plexus/plexus-sec-dispatcher/1.4/plexus-sec-dispatcher-1.4.jar
[ERROR] urls[2] = file:/C:/Users/C33016/.m2/repository/org/sonatype/plexus/plexus-cipher/1.4/plexus-cipher-1.4.jar
[ERROR] urls[3] = file:/C:/Users/C33016/.m2/repository/org/codehaus/plexus/plexus-utils/3.2.1/plexus-utils-3.2.1.jar
[ERROR] urls[4] = file:/C:/Users/C33016/.m2/repository/org/sonarsource/scanner/api/sonar-scanner-api/2.16.2.588/sonar-scanner-api-2.16.2.588.jar
[ERROR] urls[5] = file:/C:/Users/C33016/.m2/repository/commons-lang/commons-lang/2.6/commons-lang-2.6.jar
[ERROR] Number of foreign imports: 1
[ERROR] import: Entry[import from realm ClassRealm[maven.api, parent: null]]
[ERROR]
[ERROR] -----------------------------------------------------
What can I do about that?
Unfortunately, the ArgumentListTreeImpl
class is not part of the public API of sonar-java. This means that they will not be available at runtime on SQ. You should only rely on classes coming from the org.sonar.plugins.java.api
package