编写检测器以使用Findbugs搜索“System.out.println”的用法

我正在尝试使用Findbugs编写一个错误检测器来查找方法调用“System.out.println”的实例。

据我所知,字节码中的“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)); } } } } 

我发现,对于我的用例, 确定完全使用System.out或System.err足够了 – 在99%的情况下,这些将用于稍后在块中调用.print或.println。 我的检测器检测到加载System.err或System.out的GET_STATIC操作码。 代码如下,显示了确定发生这种情况的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); } } 

你的任务比看起来要复杂一些。 一个简单的案例:

 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() ); 

确实如此 – 你需要一个堆栈 ,你必须跟踪字节码指令中定义的每个推/弹操作。 为这么简单的任务做了很多工作……

但是如果你走这条路,解决这个问题很简单:当你遇到INVOKEVIRTUAL时,堆栈的顶部应该包含一些值,顶部下面的值应该是“ java/lang/System.out ”。

话虽如此,我100%肯定Findbugs已经实现了这一点,可能你可以使用一些FindBugs API来让你的生活更轻松。

使用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,则表示您处于报告错误的状态。

我在过去提出的一个解决方案发现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

我已经看到了现有的答案,我考虑改变我的解决方案。 为了完整起见,我刚添加了这个。