针对d4j的一些思考

缺陷定位

最开始的想法是直接使用failing_tests中的异常栈来提取项目代码相关的异常栈,一般来说单元测试代码中会直接复现某个函数报错,在异常栈中就可以直接定位到报错函数,比如Lang 1b的这段报错:

1
2
3
4
5
6
7
8
9
10
11
12
--- org.apache.commons.lang3.math.NumberUtilsTest::TestLang747
java.lang.NumberFormatException: For input string: "80000000"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.valueOf(Integer.java:957)
at java.base/java.lang.Integer.decode(Integer.java:1436)
at org.apache.commons.lang3.math.NumberUtils.createInteger(NumberUtils.java:684)
at org.apache.commons.lang3.math.NumberUtils.createNumber(NumberUtils.java:474)
at org.apache.commons.lang3.math.NumberUtilsTest.TestLang747(NumberUtilsTest.java:256)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
.....后面省略

可以直接定位到NumberUtilscreateNumber函数调用的createInteger函数报错,直接解析异常栈就可以得到非常准确的报错函数信息,基于此去收集上下文就非常精准。

但后来查看了其他部分测试用例,发现有一些情况下,异常栈并不直接反应报错函数,也就是说,被测函数并不直接导致报错,比如Cli 1b单元测试函数是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BugCLI13Test
extends TestCase{
public void testCLI13()
throws ParseException{
final String debugOpt = "debug";
Option debug = OptionBuilder
.withArgName( debugOpt )
.withDescription( "turn on debugging" )
.withLongOpt( debugOpt )
.hasArg()
.create( 'd' );
Options options = new Options();
options.addOption( debug );
CommandLine commandLine = new PosixParser().parse( options, new String[]{"-d", "true"} );

assertEquals("true", commandLine.getOptionValue( debugOpt ));
assertEquals("true", commandLine.getOptionValue( 'd' ));
assertTrue(commandLine.hasOption( 'd'));
assertTrue(commandLine.hasOption( debugOpt));
}
}

单元测试完整异常栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
--- org.apache.commons.cli.bug.BugCLI13Test::testCLI13
junit.framework.AssertionFailedError
at junit.framework.Assert.fail(Assert.java:55)
at junit.framework.Assert.assertTrue(Assert.java:22)
at junit.framework.Assert.assertTrue(Assert.java:31)
at junit.framework.TestCase.assertTrue(TestCase.java:201)
at org.apache.commons.cli.bug.BugCLI13Test.testCLI13(BugCLI13Test.java:50)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at junit.framework.TestCase.runTest(TestCase.java:176)
at junit.framework.TestCase.runBare(TestCase.java:141)
at junit.framework.TestResult$1.protect(TestResult.java:122)
at junit.framework.TestResult.runProtected(TestResult.java:142)
at junit.framework.TestResult.run(TestResult.java:125)
at junit.framework.TestCase.run(TestCase.java:129)
at junit.framework.TestSuite.runTest(TestSuite.java:252)
at junit.framework.TestSuite.run(TestSuite.java:247)
at org.apache.tools.ant.taskdefs.optional.junit.JUnitTestRunner.run(JUnitTestRunner.java:520)
at org.apache.tools.ant.taskdefs.optional.junit.JUnitTask.executeInVM(JUnitTask.java:1492)
at org.apache.tools.ant.taskdefs.optional.junit.JUnitTask.executeTests(JUnitTask.java:878)
at org.apache.tools.ant.taskdefs.optional.junit.JUnitTask.executeOrQueue(JUnitTask.java:1980)
at org.apache.tools.ant.taskdefs.optional.junit.JUnitTask.executeTests(JUnitTask.java:830)
at org.apache.tools.ant.taskdefs.optional.junit.JUnitTask.execute(JUnitTask.java:2287)
at org.apache.tools.ant.UnknownElement.execute(UnknownElement.java:291)
at jdk.internal.reflect.GeneratedMethodAccessor4.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.apache.tools.ant.dispatch.DispatchUtils.execute(DispatchUtils.java:106)
at org.apache.tools.ant.Task.perform(Task.java:348)
at org.apache.tools.ant.Target.execute(Target.java:392)
at org.apache.tools.ant.Target.performTasks(Target.java:413)
at org.apache.tools.ant.Project.executeSortedTargets(Project.java:1399)
at org.apache.tools.ant.Project.executeTarget(Project.java:1368)
at org.apache.tools.ant.helper.DefaultExecutor.executeTargets(DefaultExecutor.java:41)
at org.apache.tools.ant.Project.executeTargets(Project.java:1251)
at org.apache.tools.ant.Main.runBuild(Main.java:811)
at org.apache.tools.ant.Main.startAnt(Main.java:217)
at org.apache.tools.ant.launch.Launcher.run(Launcher.java:280)
at org.apache.tools.ant.launch.Launcher.main(Launcher.java:109)

可以看出报错并非是某个函数内部报错,而是断言导致的。这就导致异常栈无法找到项目代码相关的定位信息。

所以更优解是使用JavaParser解析函数调用情况,从而抽取出断言函数行所涉及到的项目函数上下文。当然,这里还可能存在多重函数嵌套的情况。

从搜索空间来讲,这种剪枝很大程度上缩小了搜索空间,但另一方面,并没有一种方案能够实现仅依赖报错堆栈信息和相关代码,就可以100%正确的缺陷定位。如果用大模型来进行定位,这部分主要还是语义理解的问题,但同样的,也可以采用端到端的思路,即直接基于上下文进行修复,通过function call让大模型在修复过程中收集想要查看的上下文,这相当于将缺陷定位合并到了修复过程中。

但这样相当于将所有的工作全都交给了LLM,显然并不合理。

前几天看到过使用蒙特卡洛树搜索进行修复的,但又联想到DeepSeek R1技术报告提到的在模型训练时使用MCTS会使得LLM 的 token 生成搜索空间巨大,这种修复方案就有点类似于贪心的最优路径搜索,感觉还是容易陷入局部最优。其实类比一下,可以将修复工作看作一个最优路径搜索,起点是待修复代码,终点是正确代码,搜索路径就是代码被修复的各个中间过程,其中存在多条可达路径,因为大部分情况下解决方案不止一种,我们所需要做的就是最高效的找到这条最优路径。