Failing to detect some API usage

Hi, good people working on SonarQube!

Let me start with saying that I am not an experienced developer of custom rules. I have only developed a few for my own needs. But here I a stuck.

I am developing a custom rule. In that, I am trying to detect and report usage of sun.awt.CGraphicsDevice.getCGDisplayID() method. Namely, I am failing to detect the classname for the method.

This is a piece of code under the analysis:

new sun.awt.CGraphicsDevice(0).getCGDisplayID();
sun.misc.Unsafe.getUnsafe().defineAnonymousClass(Object.class, new byte[0], new Object[0]);

This is how I get the classname:

((MemberSelectExpressionTree) tree.methodSelect()).expression().symbolType().fullyQualifiedName() 

where tree is a org.sonar.plugins.java.api.tree.MethodInvocationTree.

That gives me just CGraphicsDevice for the first method invocation and not sun.awt.CGraphicsDevice. It works fine for the following invocations in the code.

The other thing which I happen to notice, which may or may not be related, is that the identifier symbol is not a method symbol:

((MemberSelectExpressionTree)tree.methodSelect()).identifier().symbol().isMethodSymbol() gives me false for the first method invocation and true for the latter two method invocations. Also symbol class is org.sonar.java.model.Symbols$UnknownSymbol for the first invocation and org.sonar.java.model.JMethodSymbol for the other two.

The only other thing I was able to check, also perhaps irrelevant, is that, at the other end, Eclipse API (org.eclipse.jdt.core.dom.ASTParser) seems to be giving the same information for all method calls in the code.

Finally, since the method in question is a concealed API, I did try to add appropriate --add-opens, and even --add-exports, none of which helped. I did that for the scanner VM, because that’s where, from what I could understand, the rules code is executed with reflection.

Lastly, you may find that the method in question has been removed in JDK12. I was running all the code with a JDK in which the method still exists.

  • Can you advice me how to get the classname for that method (and parameter types).?
    – or –
  • Can you point me to a piece of code where similar things are done?
    – or –
  • Can you point me to the code where it is decided if symbol is a JMethodSymbol - from that I may be able to understand what is going on fundamentally.

Really hope you can help!

Thank you very much in advance!

Let me know if more information needed.

This is the mandatory information:

Must-share information:

  • which versions are you using 8.9.10.61524
  • how is SonarQube deployed: zip, Docker, Helm: zip
  • what are you trying to achieve: write a custom rule to detect usage of some API
  • what have you tried so far to achieve this: debug the code, add debug output, reading API, looking over samples, searching sonar-java source, etc

Shura

The decision seems made in org.sonar.java.model.expression.MethodInvocationTreeImpl.symbol():

    return methodBinding != null
      ? root.sema.methodSymbol(methodBinding)
      : Symbols.unknownMethodSymbol;

methodBinding seems to be set in org.sonar.java.mode.JParser.convertStatement(Statement):

      case ASTNode.METHOD_INVOCATION: {
        MethodInvocation e = (MethodInvocation) node;
...
        MethodInvocationTreeImpl t = new MethodInvocationTreeImpl(
...
        );
        t.methodBinding = excludeRecovery(e.resolveMethodBinding(), arguments.size());

And then in org.sonar.java.mode.JParser.excludeRecovery(IMethodBinding, int), we have:

    if (methodBinding == null) {
      return null;
    }

It looks like the only question left is why org.eclipse.jdt.core.dom.MethodInvocation.resolveMethodBinding() returns null for the method in question.

Looks like I am onto studying the eclipse API.

With some mixed feelings I am reporting that the eclipse API seems to be successfully able to detect the method. This test code:

package test;

import org.eclipse.jdt.core.dom.*;
import java.util.*;

public class Main {

    public final static String SOURCE =
            "package test;\n" +
            "import sun.awt.CGraphicsDevice;\n" +
            "import sun.misc.Unsafe;\n" +
            "import java.util.ArrayList;\n" +
            "class JDK11API {\n" +
            "    public JDK11API(String name) {\n" +
            "        new CGraphicsDevice(0).getCGDisplayID();\n" +
            "        new ArrayList(0).size();\n" +
            "    }\n" +
            "}\n";

    public static void main(String[] args) throws ClassNotFoundException {
        //making sure that a correct Java distribution is used
        System.out.println(System.getProperty("java.home"));
        //only look on the methods which are interesting
        Map<String, String> expectedMethods = Map.of("getCGDisplayID", "sun.awt.CGraphicsDevice",
                "size", "java.util.ArrayList");
        //making sure the methods in question exist
        for(Map.Entry<String, String> e : expectedMethods.entrySet())
            if (Arrays.stream(Class.forName(e.getValue()).getDeclaredMethods())
                    .anyMatch(m ->
                            m.getName().equals(e.getKey()) && m.getParameterTypes().length == 0))
                System.out.println(e.getValue() + "." + e.getKey() + "() exists.");
            else {
                System.out.println(e.getValue() + "." + e.getKey() + "() does not exist.");
                System.exit(1);
            }
        //init parser
        ASTParser parser = ASTParser.newParser(AST.JLS15);
        parser.setEnvironment(new String[0], new String[0], new String[0], true);
        parser.setResolveBindings(true);
        parser.setBindingsRecovery(true);
        parser.setSource(SOURCE.toCharArray());
        parser.setUnitName("/test/JDK11API.java");
        //parse and print the obtained information
        CompilationUnit node = (CompilationUnit) parser.createAST(null);
        //expecting the very first type to be the class under test, otherwise need to look for the right class
        TypeDeclaration clss = (TypeDeclaration) node.types().get(0);
        //expecting the very first method to be the method under test, otherwise need to look for the right method
        MethodDeclaration mthd = clss.getMethods()[0];
        for (Object o : mthd.getBody().statements()) {
            Statement s = (Statement) o;
            s.accept(new ASTVisitor() {
                @Override
                public boolean visit(MethodInvocation node) {
                    String methodName = node.getName().toString();
                    String className = node.getExpression().resolveTypeBinding().getQualifiedName();
                    if (expectedMethods.get(methodName).equals(className))
                        System.out.println("Discovered method: " +
                                 className + "." + methodName + "()");
                    else {
                        System.out.println("Method " + methodName + "parsed incorrectly");
                    }
                    return super.visit(node);
                }
            });
        }
    }
}

Gives me this:

.../jdk-11
sun.awt.CGraphicsDevice.getCGDisplayID() exists.
java.util.ArrayList.size() exists.
Discovered method: sun.awt.CGraphicsDevice.getCGDisplayID()
Discovered method: java.util.ArrayList.size()

With that I have to go back to the Java plugin code to find out how the ASMParser is initialized.

Sigh …

1 Like

Hey @shurymury ,

Sorry for coming to you so late after your initial request, and thank you for your patience. Did you manage to make it work ultimately?

If not, I might try to give a hand in digging into the Java analyzer’s inner logic. In order to help you, can you also share the entire code of your custom rule (or only the parts that you can share… but a least the entire logic until the method invocation decision making), as well as the test code you use for your rule (input file). I would also like to see how you configure your unit test for this particular rule if it differs from others.

Also, did you see a difference in terms of behavior between the unit test of the custom rule, and the rule behavior at runtime (when deploying the custom plugin and analyzing a project containing expected issues). I suspect we might have an issue with the way we configure the rule verifier itself.

Anway, congrats on your tests using the eclipse parser and digging into it. It shows that the issue is indeed most probably on the analyzer side, and not on ECJ’s side (and that’s good for us).

Cheers,
Michael