Написание детектора для поиска использования «System.out.println» с использованием Findbugs

#java #bytecode #findbugs

#java #байт-код #findbugs

Вопрос:

Я пытаюсь написать детектор ошибок, чтобы найти экземпляры вызова метода «System.out.println» с использованием Findbugs.

Я понимаю, что «System.out.println» в байт-коде компилируется для вызова GETSTATIC, который помещает «System.out» в стек. Вызов INVOKEVIRTUAL извлекает «System.out» из стека и вызывает метод.

Я подготовил некоторый код (см. ниже), который находит правильные вызовы GETSTATIC и INVOKEVIRTUAL, но не смог связать их вместе. Я подозреваю, что мне может понадобиться каким-то образом использовать OpcodeStack, но у меня возникают проблемы с пониманием того, как я могу его использовать. Любая помощь была бы оценена.

     @Override 
    public void sawOpcode(int seen) { 
            // if opcode is getstatic 
            if (seen == GETSTATIC) { 
                    String clsName = getClassConstantOperand(); 
                    if ("java/lang/System".equals(clsName)) { 
                            String fldName = getNameConstantOperand(); 
                            if ("out".equals(fldName)) { 
                                    System.out.println("SYSTEM.OUT here"); 
                            } 
                    } 
            } 

            // if opcode is invokevirtual 
            if (seen == INVOKEVIRTUAL) { 
                    String cls = getDottedClassConstantOperand(); 
                    if ("java.io.PrintStream".equals(cls)) { 
                            String methodName = getNameConstantOperand(); 
                            if ("println".equals(methodName)) { 
                                    bugReporter.reportBug(new BugInstance("SYSTEM_OUT_PRINTLN", 
                                                    NORMAL_PRIORITY).addClassAndMethod(this) 
                                                    .addSourceLine(this)); 
                            } 
                    } 
            } 

    }
  

Комментарии:

1. обязательно ли запускать его в байт-коде или его можно запустить в исходных файлах?

2. Извините, отредактировал сообщение, я забыл указать, что я использовал Findbugs (таким образом, он должен выполняться в байт-коде). Спасибо.

3. ОК. потому что PMD имеет для этого совершенно правильное правило pmd.sourceforge.net/rules/logging-java.html

4. Спасибо, однако мне интересно узнать, как детектор будет выглядеть в Findbugs, поскольку с этим «простым» примером я мог бы работать над более сложными.

Ответ №1:

Я обнаружил, что для моего варианта использования достаточно определить, что System.out или System.err используются вообще — в 99% случаев они будут использоваться для вызова .print или .println позже в блоке. Мой детектор обнаруживает коды операций GET_STATIC, которые загружают System.err или System.out. Ниже приведен код, показывающий 3 варианта определения того, что это происходит.

 package my.findbugs.detectors.forbiddencalls;

import org.apache.log4j.Logger; // it is not trivial to use a logger with FindBugs in Eclipse, leave it out if there are problems

import my.findbugs.detectors.util.DetectorUtil;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.bcel.OpcodeStackDetector;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.FieldDescriptor;


public class CallToSystemOutPrintlnDetector2 extends OpcodeStackDetector {

    private static final Logger LOGGER = Logger.getLogger(CallToSystemOutPrintlnDetector2.class);
    private BugReporter bugReporter;


    public CallToSystemOutPrintlnDetector2(BugReporter bugReporter) {
        super();
        this.bugReporter = bugReporter;
        LOGGER.debug("Instantiated.");
    }


    public void sawOpcode(int seen) {

        // find occurrences of:  
        //2:   getstatic       #54; //Field java/lang/System.out:Ljava/io/PrintStream;
//2:   getstatic       #54; //Field java/lang/System.out:Ljava/io/PrintStream;

        if (seen == GETSTATIC){

            try {
//              LOGGER.debug(operand); // static java.lang.System.out Ljava/io/PrintStream;
//              LOGGER.debug(operand.getClass()); // class edu.umd.cs.findbugs.classfile.analysis.FieldInfo
//              LOGGER.debug(operand.getName()); // err
//              LOGGER.debug(operand.getClassDescriptor()); // java/lang/System
//              LOGGER.debug(operand.getSignature()); // Ljava/io/PrintStream;

                FieldDescriptor operand = getFieldDescriptorOperand();
                ClassDescriptor classDescriptor = operand.getClassDescriptor();
                if ("java/lang/System".equals(classDescriptor.getClassName()) amp;amp; 
                        ("err".equals(operand.getName())||"out".equals(operand.getName()))) {
                    reportBug();
                }
            } catch (Exception e) {
                //ignore
            }

            // could be used
//          try {
//              MethodDescriptor operand = getMethodDescriptorOperand();
//              LOGGER.debug(operand); // java.lang.System.outLjava/io/PrintStream;
//              LOGGER.debug(operand.getClass()); // class edu.umd.cs.findbugs.classfile.MethodDescriptor
//              LOGGER.debug(operand.getName()); // err 
//              LOGGER.debug(operand.getClassDescriptor()); // java/lang/System
//              LOGGER.debug(operand.getSignature()); // Ljava/io/PrintStream;
//          } catch (Exception e) {
//              //ignore
//          }

            // could be used
//          try {
//              String operand = getRefConstantOperand();
//              LOGGER.debug(operand); // java.lang.System.out : Ljava.io.PrintStream;
//              if (operand != null amp;amp; (
//                  operand.startsWith("java.lang.System.out :") || operand.startsWith("java.lang.System.err :"))) {
//                  reportBug();
//              }
//          } catch (Exception e) {
//              //ignore
//          }
        }
    }

    private void reportBug(){
        this.bugReporter.reportBug(getBugInstance());
    }


    private BugInstance getBugInstance() {
        return new BugInstance(this, "MY_CALL_TO_SYSTEM_OUT_BUG", DetectorUtil.MY_PRIORITY)
            .addClassAndMethod(this)
            .addSourceLine(this);
    }

}
  

Ответ №2:

Ваша задача немного сложнее, чем кажется. Простой случай:

 System.out.println("abc");
  

Также преобразуется в простой байт-код:

 getstatic   #2; //java/lang/System.out
ldc #3; //String abc
invokevirtual   #4; //Calling java/io/PrintStream.println(String)
  

Однако, если вы пытаетесь напечатать что-либо, кроме простого постоянного / известного значения, это становится сложнее:

 int x = 42;
System.out.println(x   17);
  

Будет переведено на:

 bipush  42
istore_1  //x = 42
getstatic   #2; //java/lang/System.out
iload_1  //x
bipush  17
iadd  //x   17 on the stack
invokevirtual   #5; //Calling java/io/PrintStream.println(int)
  

Но подождите, это может ухудшиться:

 System.out.println("x times 27 is "   x * 27);
  

Что? StringBuilder : ?

 new #6; //new java/lang/StringBuilder()
dup
invokespecial   #7; //Calling java/lang/StringBuilder()
ldc #8; //String x times 2 is
invokevirtual   #9; //Calling java/lang/StringBuilder.append(String)
iload_1  //x
bipush  27
imul  //x * 27 on the stack
invokevirtual   #10; //Calling java/lang/StringBuilder.append:(int) with 'x * 27' argument
invokevirtual   #11; //Calling java/lang/StringBuilder.toString:()
invokevirtual   #4; //Calling java/io/PrintStream.println(String)
  

Интересно, что исходный код был переведен на (который является известной оптимизацией Java 5 (?)):

 System.out.println(
  new StringBuilder().
    append("x times 27 is ").
    append(x * 27).
    toString()
  );
  

Решение

Действительно, вам понадобится стек, и вам придется отслеживать каждую операцию push / pop, как определено в инструкции по байт-кодированию. Много работы для такой простой задачи…

Но если вы пойдете по этому пути, решение проблемы довольно простое: когда вы сталкиваетесь с INVOKEVIRTUAL, верхняя часть стека должна содержать некоторое значение, а значение под верхней частью должно быть « java/lang/System.out «.

При этом я на 100% уверен, что Findbugs уже реализовал это, и, вероятно, вы можете использовать какой-нибудь FindBugs API, чтобы упростить вашу жизнь.

Комментарии:

1. Я хотел бы подтвердить, что я ранее проверял наличие такого детектора здесь: findbugs.sourceforge.net/bugDescriptions.html а также путем написания тестового примера, содержащего «System.out.println», и он не был помечен как ошибка. Я знаю, что проблема возрастает в зависимости от печатаемой строки. Я могу открыть верхнюю часть стека, используя: OpcodeStack. Элемент item = stack.getStackItem(0); однако у меня возникают некоторые трудности при сравнении его с тем, что было помещено в стек с помощью GETSTATIC. Спасибо за ваш подробный ответ.

Ответ №3:

Используйте класс OpcodeStack.

Когда вы увидите GETSTATIC и поймете, что у вас есть «out», установите пользовательское значение в сгенерированном OpcodeStack.Элемент в Boolean.TRUE. для этого выполните

 try {
     //process opcodes
} finally {
    super.sawOpcode(seen);
    if (pseudocode-saw System.out.println) {
        OpcodeStack.Item item = stack.getStackItem(0);
        item.setUserValue(Boolean.TRUE);
}
  

затем, когда вы обрабатываете вызов println, посмотрите на tos, и установлено ли значение пользователя в логическое значение.ВЕРНО, вы знаете, что находитесь в состоянии сообщить об ошибке.

Ответ №4:

Одно из решений, которое я придумал в прошлом, находит System.out.println вызовы, в которых «не слишком много вычислений внутри круглых скобок», т. Е. Где между ними находится максимум инструкций MAX_WILDCARDS.

Я расширил ByteCodePatternDetector, мой шаблон был следующим:

 ByteCodePattern pattern = new ByteCodePattern();
// as this is a pattern, I cannot specify here that this is supposed to be System.out
pattern.add(new Load(SYSO_FIELD, "sysoResult")); 
pattern.add(new Wild(MAX_WILDCARDS)); 
pattern.add(new Invoke("java.io.PrintStream", "println", "/.*", Invoke.INSTANCE, null).label(LABEL_PRINT));
  

Позже я удостоверяюсь, что байтовые коды для операции загрузки и вызова являются правильными, и сравниваю класс и имя поля, которые я извлекаю из match.getBindingSet().lookup(...) , чтобы убедиться, что он System.out вызывается.

Я видел существующие ответы, и я рассматриваю возможность изменения своего решения. Я просто добавил это для полноты картины.