Написание детектора для поиска использования «System.out.println» с использованием Findbugs - PullRequest
5 голосов
/ 09 октября 2011

Я пытаюсь написать детектор ошибок, чтобы найти экземпляры вызова метода «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)); 
                            } 
                    } 
            } 

    }

Ответы [ 4 ]

1 голос
/ 18 мая 2013

Я обнаружил, что для моего случая использования достаточно, чтобы определить, что 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()) && 
                        ("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 && (
//                  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);
    }

}
1 голос
/ 09 октября 2011

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

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, вершина стека должна содержать некоторое значение, а значение нижеtop должен быть "java/lang/System.out".

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

0 голосов
/ 18 апреля 2013

Одно решение, которое я придумал в прошлом, находит 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));

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

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

0 голосов
/ 15 октября 2011

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

Когда вы видите GETSTATIC и понимаете, что у вас есть «out», установите значение пользователя в сгенерированном OpcodeStack.Item равным 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, и если пользовательское значение установлено в Boolean.TRUE, вы знаете, что находитесь в состоянии сообщить об ошибке.

...