文章目录
细粒度和准确的源代码差异
1 概念
1.1 软件演化(Sofware evolution)
1.1.1 核心
对源代码文件进行的一系列编辑操作,称为编辑脚本。
1.1.1 分类
全局软件演化
本地软件演化
1.2 全局软件演化
需求、执行环境演化
1.3 本地软件演化
源代码文件的演化,本文关注
1.4 编辑脚本(Edit script)
对源代码文件进行的一系列编辑操作
对源代码文件进行编辑操作的序列
1.4.1 编辑操作分类
添加行操作
删除行操作
移动操作
更新操作
1.4.2 粒度
文本行
抽象语法树(AST)
细粒度
大型
1.4.3 目标
由于软件存储在版本控制系统中,所以编辑脚本是在同一文件的两个版本之间计算的。编辑脚本的目标是准确反映对文件执行的实际更改。
1.4.4 算法
-
文本行粒度
Unix diff工具以执行Myers算法
局限性
- 只计算添加和删除,而不考虑其他类型的编辑操作,如更新和移动。
- 它的粒度(文本行)是粗粒度的,与源代码结构(抽象语法树)不一致。
-
AST粒度
GumTree算法
挑战
- 处理移动操作
- 扩展到具有数千个节点的细粒度AST
主要优点
编辑脚本直接引用代码的结构
1.5 抽象语法树(AST)
是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
AST是一个带标记有序有根树,其中节点可能有一个字符串值。
节点的标签对应于语法中生成规则的名称,即它们对结构进行编码。节点的值对应于代码中的实际标记。
T是树每个结点都有一个标签和一个值(可能为空),标签对于代码结构比如函数声明,代码块等
1.5.2 粒度
AST可以具有不同的粒度,一个节点可以编码整个指令或更精细的粒度表达式。例如,return "Foo!";
语句,可以用类型为Statement
和值return "Foo!"
的单个节点进行编码,或者像我们的示例中那样使用两个节点(参见节点b)。如果此语句更改为return "Foo!" + i;
,只有细粒度表示才能看到添加了InfixExpression:+
和SimpleName:i
节点。
1.6 AST差异
AST差异基于AST编辑操作的概念。它旨在计算将AST转换为另一个AST的一系列编辑操作。此序列称为编辑脚本。
1.6.1 编辑操作
- u p d a t e V a l u e ( t , v n ) updateValue(t,v_n) updateValue(t,vn)将 t t t的旧值替换为新值 v n v_n vn
- a d d ( t , t p , i , l , v ) add(t,t_p,i,l,v) add(t,tp,i,l,v)在AST中添加一个新节点 t t t。如果 t p t_p tp不为空并且指定了 i i i,则 t t t是 t p t_p tp的 i t h i^{th} ith子节点。否则 t t t是新的根节点,并将前一个根节点作为其唯一的子节点。最后, l l l是 t t t的标签, v v v是 t t t的值。
- d e l e t e ( t ) delete(t) delete(t)删除AST的叶节点。
- m o v e ( t , t p , i ) move(t,t_p,i) move(t,tp,i)移动节点 t t t并使其成为 t p t_p tp的 i t h i^{th} ith子节点。请注意, t t t的所有子级也会移动,因此此操作会移动整个子树。
1.6.2 步骤
首先,它们在两个AST的相似节点之间建立映射(节点对)。这些映射有两个约束:一个给定的节点只能属于一个映射,而映射涉及具有相同标签的两个节点。其次,基于这些映射,他们推断出必须在第一个AST上执行的编辑脚本才能获得第二个AST。
1.7 GumTree算法
寻找两个AST之间的映射,两个连续阶段
图1:两个示例Java文件及其相应的AST和映射。仅具有标签的节点表示为:Label
,具有标签和值的节点表示:Lable: value
。自顶向下阶段的映射用长虚线表示(这些节点的后代也被映射,但为了增强可读性,省略了它)。自底向上阶段的映射使用**短虚线(容器映射)或交替虚线(恢复映射)**进行描述。不匹配的节点呈灰色。
1.7.1 自顶向下
贪心算法,贪心地搜索** T 1 T_1 T1和 T 2 T_2 T2之间的最大同构子树。在这些同构子树的节点之间**建立映射。它们被称为锚映射。
1 树高度、节点高度 t ∈ T t∈T t∈T
- 叶节点 t t t, h e i g h t ( t ) = 1 \mathbf{height}(t)=1 height(t)=1;高度为1
- 内部节点 t t t, h e i g h t ( t ) = max ( { h e i g h t ( c ) ∣ c ∈ c h i l d r e n ( t ) } ) + 1 \mathbf{height}(t)=\max(\{\mathbf{height}(c)|c∈ children(t)\})+1 height(t)=max({height(c)∣c∈children(t)})+1。高度为最大子节点高度加1.
2 辅助数据结构
高度索引优先级列表。此列表包含按高度递减顺序排列的节点序列。
3 函数
- p u s h ( t , l ) push(t,l) push(t,l)将节点 t t t插入列表 l l l中。
- p e e k M a x ( l ) peekMax(l) peekMax(l)返回列表的最大高度。
- p o p ( l ) pop(l) pop(l)返回并从 l l l中移除高度等于 p e e k M a x ( l ) peekMax(l) peekMax(l)的 l l l的所有节点的集合。
- o p e n ( t , l ) open(t,l) open(t,l)将 t t t的所有子代插入到 l l l中。
- dice函数,该函数测量给定一组映射 M \mathcal M M的两个节点之间的公共后代的比率,即 d i c e ( t 1 , t 2 , M ) = 2 × ∣ t 1 ∈ s ( t 1 ) ∣ ( t 1 , t 2 ) ∈ M ∣ s ( t 1 ) ∣ + ∣ s ( t 2 ) ∣ dice(t_1,t_2,\mathcal M)=\frac{2×|{t_1∈s(t_1)|(t_1,t_2)∈\mathcal M}}{|s(t_1)|+|s(t_2)|} dice(t1,t2,M)=∣s(t1)∣+∣s(t2)∣2×∣t1∈s(t1)∣(t1,t2)∈M,其中 s ( t i ) s(t_i) s(ti)是节点 t i t_i ti的后代集合。dice系数范围在 [ 0 , 1 ] [0,1] [0,1]实数区间内,值为1表示 t 1 t_1 t1的后代集合与 t 2 t_2 t2的后代集合相同。算法如算法1所示。
4 算法
首先将AST T 1 T_1 T1与 T 2 T_2 T2的根分别插入列表 L 1 L_1 L1和 L 2 L_2 L2。
当两个列表的最大高度有都大于等于最小高度时,说明可能有子树是同构的继续循环。
若两个列表最大高度不相等,将两个列表中高度最大的结点移除,然后将所有子节点插入。
若列表最大高度相等,则判断是否同构,首先判断两个列表中结点的任意组合是否同构,若同构且有一个结点可以与多个结点同构则将多个映射加入候选映射列表,否则加入锚映射列表。然后将所有不在映射列表中的结点的子节点打开插入 L 1 L_1 L1和 L 2 L_2 L2,找子节点的映射。
一对多的映射,已经在候选列表中,将父节点公共后代比率最大的结点加入映射列表中。对每个候选映射的父级上使用dice函数,将候选映射列表进行排序,具有较大值的映射优先。然后,移除第一个元素,将其添加到映射集中,移除涉及此映射的节点的映射,直到候选映射列表为空。
1.7.2 自底向上
其中两个节点匹配(称为容器映射),前提是它们的后代(节点的子节点,以及它们的子节点等等)包含大量公共锚。当两个节点匹配时,我们最终应用一个优化算法在它们的后代中搜索其他映射(称为恢复映射)。
自顶向下阶段产生的映射被作为输入
首先,寻找容器映射,具有大量匹配的子节点。==对于找到的每个容器映射,我们都会查找恢复映射,这些映射在映射节点的仍然不匹配的后代中进行搜索。==为了找到容器映射, T 1 T_1 T1的节点按后序处理。对于 T 1 T_1 T1的每个不匹配的非叶节点,我们从 T 2 T_2 T2中提取候选节点列表。一个节点 c ∈ T 2 c∈T_2 c∈T2如果满足 l a b e l ( t 1 ) = l a b e l ( c ) label(t_1)=label(c) label(t1)=label(c), c c c不匹配,并且 t 1 t_1 t1和 c c c有一些匹配的后代,则它是 t 1 t_1 t1的候选。然后我们选择具有最大 d i c e ( t 1 , t 2 , M ) \mathbf{dice}(t_1,t_2,\mathcal M) dice(t1,t2,M)值的候选 t 2 ∈ T 2 t_2∈T_2 t2∈T2。如果 d i c e ( t 1 , t 2 , M ) > m i n D i c e \mathbf{dice}(t_1,t_2,\mathcal M)>minDice dice(t1,t2,M)>minDice, t 1 t_1 t1和 t 2 t_2 t2匹配在一起。为了搜索 t 1 t_1 t1和 t 2 t_2 t2的后代之间的其他映射,我们首先删除它们匹配的后代,如果两个子树的大小都小于 m a x S i z e maxSize maxSize,我们应用一个表示为 o p t opt opt的算法,该算法可以找到一个没有移动操作的最短编辑脚本。在我们的实现中,我们使用RTED算法[27]。如果从这个编辑脚本中导出的映射涉及具有相同标签的节点,则将其添加到 M \mathcal M M中。
1.7.3 自底向上阶段
算法目的:寻找容器映射和恢复映射。
输入:两个AST T 1 T_1 T1和 T 2 T_2 T2,映射集 M \mathcal M M,最小阈值 m i n D i c e minDice minDice,最大树大小 m a x S i z e maxSize maxSize。
输出:映射集 M \mathcal M M
算法步骤:
- 自底向上后续遍历 T 1 T_1 T1,寻找 T 1 T_1 T1的不匹配结点 t 1 t_1 t1但是有匹配的子节点。( t 1 t_1 t1不可能是锚映射)
- 从 T 2 T_2 T2中提取 t 1 t_1 t1的候选结点,候选结点与 t 1 t_1 t1不匹配,但是具有一些匹配的后代。选择具有最多公共后代的 t 2 t_2 t2。
- 如果 t 1 t_1 t1和 t 2 t_2 t2的公共后代比率超过阈值 m i n D i c e minDice minDice,说明匹配度较高,作为容器映射加入 M \mathcal M M中。
- 寻找恢复映射:删去 t 1 t_1 t1和 t 2 t_2 t2的公共后代后,若树大小都小于 m a x S i z e maxSize maxSize,用RTED算法找到具有相同标签的结点(可以没有相同的值,如图里的public->private,对应了更新值操作),作为恢复映射加入到 M \mathcal M M中。
1.7.4 复杂度
复杂度: O ( n 2 ) O(n^2) O(n2),不匹配结点的笛卡尔积 O ( n 2 ) O(n^2) O(n2),RTED算法 O ( d ∗ m 3 ) O(d*m^3) O(d∗m3)对小于 m a x S i z e maxSize maxSize的子树使用(m是容器映射中树的大小, d d d是容器映射数量,实际情况下有 m < m a x S i z e < < n m<maxSize<<n m<maxSize<<n,因此可以忽略该三次方复杂度)。
算法1复杂度: O ( n 2 ) O(n^2) O(n2),需要对相同高度的结点笛卡尔积运算 O ( n 2 ) O(n^2) O(n2)。
计算编辑脚本复杂度: O ( n 2 ) O(n^2) O(n2)。
1.7.4 编辑脚本
红色长虚线:锚映射
蓝色短虚线:容器映射
绿色交替虚线:恢复映射
a
d
d
(
t
1
a
,
1
,
R
e
t
u
r
n
S
t
a
t
e
m
e
n
t
,
ϵ
)
a
d
d
(
t
2
,
t
1
,
0
,
S
t
r
i
n
g
L
i
t
t
e
r
a
l
,
B
a
r
)
a
d
d
(
t
3
,
a
,
2
,
I
f
S
t
a
t
e
m
e
n
t
,
ϵ
)
a
d
d
(
t
4
,
t
3
,
0
,
I
n
f
i
x
E
x
p
r
e
s
s
i
o
n
,
=
=
)
a
d
d
(
t
5
,
t
4
,
0
,
S
i
m
p
l
e
N
a
m
e
,
i
)
a
d
d
(
t
6
,
t
4
,
1
,
P
r
e
f
i
x
E
x
p
r
e
s
s
i
o
n
,
−
)
a
d
d
(
t
7
,
t
6
,
0
,
N
u
m
b
e
r
L
i
t
e
r
r
a
l
,
1
)
m
o
v
e
(
b
,
t
3
,
1
)
u
p
d
a
t
e
V
a
l
u
e
(
c
,
p
r
i
v
a
t
e
)
add(t_1a,1,ReturnStatement,\epsilon)\\ add(t_2,t_1,0,StringLitteral,Bar)\\ add(t_3,a,2,IfStatement,\epsilon)\\ add(t_4,t_3,0,InfixExpression,==)\\ add(t_5,t_4,0,SimpleName,i)\\ add(t_6,t_4,1,PrefixExpression,−)\\ add(t_7,t_6,0,NumberLiterral,1)\\ move(b,t_3,1)\\ updateValue(c,private)\\
add(t1a,1,ReturnStatement,ϵ)add(t2,t1,0,StringLitteral,Bar)add(t3,a,2,IfStatement,ϵ)add(t4,t3,0,InfixExpression,==)add(t5,t4,0,SimpleName,i)add(t6,t4,1,PrefixExpression,−)add(t7,t6,0,NumberLiterral,1)move(b,t3,1)updateValue(c,private)
将
t
1
t_1
t1结点(标签ReturnStatement(return语句)值null)加入a结点第1子树
将 t 2 t_2 t2结点(标签StringLitteral(字符串)值Bar)加入 t 1 t_1 t1结点第0子树
将 t 3 t_3 t3结点(标签StringLitteral(if语句)值Bar)加入 a a a结点第0子树
将 t 4 t_4 t4结点(标签InfixExpression(中缀表达式)值==)加入 t 3 t_3 t3结点第0子树
将 t 5 t_5 t5结点(标签SimpleName(变量名)值i)加入 t 4 t_4 t4结点第0子树
将 t 6 t_6 t6结点(标签PrefixExpression(前缀表达式)值-)加入 t 4 t_4 t4结点第1子树
将 t 7 t_7 t7结点(标签NumberLiterral(前缀表达式)值-)加入 t 6 t_6 t6结点第0子树
将 b b b结点移动到 t 3 t_3 t3结点的第1子树
更新 c c c结点的值为private