情感测试
情感测试

您现在的位置: 情感测试简介_情感测试玩法 > 情感测试简介 > 用于修补代码和评估代码质量的抽象语法树

用于修补代码和评估代码质量的抽象语法树

发布时间:2021-8-14 13:23:43   点击数:
作者

AbdulQadir译者

张健欣策划

田晓旭我们如何轻松地大规模地修补,行代码?通过阅读本文,了解我们如何使用一个简单但强大的数据结构——抽象语法树(AbstractSyntaxTree,AST)来创建一个系统,从单个中心点映射源代码依赖项,然后修补所有依赖项。

一个软件系统通常是围绕如何编写依赖项(例如底层语言系统、框架、库等等)而构建的。这些依赖项的变动可能会对软件系统本身造成连锁反应。例如,最近,著名的Python库pandas发布了其1.0.0版本,该版本弃用并更改了其先前0.25.x版本中的一些功能。一个组织可能有许多系统使用0.25.x版本的pandas。因此,将其升级到1.0.0需要每个系统的开发人员仔细阅读pandas的变更文档并相应地修补他们的代码。

由于我们开发人员喜欢将繁琐的任务自动化,所以我们自然会考虑编写一个补丁脚本,根据新的

pandas版本中的变动升级所有系统的源代码。补丁脚本可以解析源代码并执行某些查找+替换操作。但是这样的脚本可能是不可靠也不全面的。例如,假设补丁脚本需要将一个函数的名字从get改为create,包括任何其被调用的地方。简单的查找+替换操作会替换单词“get”,即使它不是一个函数调用。另外一个例子是,查找+替换操作不能处理代码语句溢出为多行的情况。我们需要补丁脚本解析源代码,同时理解语言结构。在本文中,我们建议使用抽象语法树(AbstractSyntaxTrees,AST)来写这些补丁脚本。稍后,我们将介绍如何使用AST来评估代码质量。

1抽象语法树(AST)

抽象语法树(AbstractSyntaxTree,或AST)是源代码的一种树形展示。

几乎每种语言都有一种方法根据代码生成AST。我们使用Python来构建我们的系统的一些关键部分。因此,本文使用Python来给出示例和亮点,但是这些知识也可以应用到任何其它语言。

Python有一个名为ast的包来生成ASTs。这里有一个关于它的小教程。

代码:

importast#Simplecodethatsetsavariablevarto1andthenprintsit.code="""var=1print(var)"""#ConvertscodetoAST.ObjectheadpointstotheheadoftheAST.head=ast.parse(code)print(head)

输出:

_ast.Moduleobjectat...

所以,AST的头是一个Module对象。让我们深入研究。这个ast包提供了一个ast.dump(node)函数,该函数返回以这个节点为根节点的整个树的格式化视图。我们在head对象上调用这个函数,看看我们能得到什么。

代码:

print(ast.dump(head))

输出(美化过):

Module(body=[Assign(targets=[Name(id=var,ctx=Store())],value=Num(n=1)),Expr(value=Call(func=Name(id=print,ctx=Load()),args=[Name(id=var,ctx=Load())],keywords=[]))])

查看ast.dump的输出,我们可以看到类型为Module的head对象有一个body属性,其值是一个包含2个节点的列表——一个表示var=1,另一个表示print(var)。第一个表示var=1的节点有一个target属性表示LHSvar,一个value属性表示RHS1。让我们看看能不能打印这个RHS。

代码:

print(head.body[0].value.n)

输出:

1

所以,它如预期生效。现在,我们尝试将RHS的值从1修改为2。

代码:

head.body[0].value.n=2print(ast.dump(head))

输出(美化过):

Module(body=[Assign(targets=[Name(id=var,ctx=Store())],value=Num(n=2)),Expr(value=Call(func=Name(id=print,ctx=Load()),args=[Name(id=var,ctx=Load())],keywords=[]))])

我们可以看到相应属性的值已经改为2。现在,我们想要将AST转换回代码来获得修改后的代码。为此,我们使用了一个名为astunparse的Python包,因为ast没有提供这个功能。

代码:

importastunparseprint(astunparse.unparse(head))

输出:

var=2print(var)

因此,修改后的代码的语句预期为var=2而不是var=1。

2智能补丁

既然我们已经理解了ASTs,以及如何生成AST、检查AST、修改AST并根据AST重新生成代码,让我们回到编写补丁脚本的问题上来,将系统代码修改为使用pandas1.0.0而不是pandas0.25.x。我们称这些基于AST的补丁脚本为“智能补丁(IntelliPatch)”。

pandas1.0.0中的所有向后兼容性都列在这个页面。让我们以列表中的第一个向后兼容性为例来写这种智能补丁。

避免使用MultiIndex.levels的名字

在pandas1.0.0中,一个MultiIndexlevel的名字不能使用=更新,而是需要使用Index.set_names()。

使用pandas0.25.x的代码:

importpandasaspdmi=pd.MultiIndex.from_product([[1,2],[a,b]],names=[x,y])print(mi.levels[0].name)mi.levels[0].name="newname"print(mi.levels[0].name)

输出:

xnew?name

上述代码在pandas1.0.0中会产生一个RunTimeError。为了使用pandas1.0.0,它要修改为如下代码。

等价于使用pandas1.0.0的代码:

import?pandas?as?pdmi?=?pd.MultiIndex.from_product([[1,?2],?[a,?b]],?names=[x,?y])print(mi.levels[0].name)mi?=?mi.set_names("new?name",?level=0)print(mi.levels[0].name)

IntelliPatch需要执行以下操作:

创建给定代码的AST并遍历它。

找出任何表示.levels[].name=形式代码的所有节点。

将第二步找到的所有节点替换为=.set_names(,level=)形式代码的节点。

下面是这样做的IntelliPatch脚本。

intelli_patch.py

importastdefis_multi_index_rename_node(node):"""Checksifthegivennoderepresentsthecode:var.levels[idx].name=valandreturnsthecorrespondingvar,idxandvalifitdoes."""try:if(isinstance(node,ast.Assign)andnode.targets[0].attr=="name"andnode.targets[0].value.value.attr=="levels"):var=node.targets[0].value.value.value.ididx=node.targets[0].value.slice.value.nval=node.valuereturnTrue,var,idx,valexcept:passreturnFalse,None,None,Nonedefget_new_multi_index_rename_node(var,idx,val):"""ReturnsASTnodethatrepresentsthecode:var=var.set_names(val,level=idx)forthegivenvar,idxandval."""returnast.Assign(targets=[ast.Name(id=var)],value=ast.Call(func=ast.Attribute(value=ast.Name(id=var),attr="set_names"),args=[val],keywords=[ast.keyword(arg="level",value=ast.Num(n=idx))],),)defpatch(node):"""TakesanASTrootedatthegivenodeandpatchesit."""#Ifitisaleafnode,thennopatchingneeded.ifnothasattr(node,"_fields"):returnnode#Foreverychildofthenode,modifyitifneededandrecursivelycallpatchonit.for(name,field)inast.iter_fields(node):ifisinstance(field,list):foriinrange(len(field)):check,var,idx,val=is_multi_index_rename_node(field[i])ifcheck:field[i]=get_new_multi_index_rename_node(var,idx,val)else:patch(field[i])else:check,var,idx,val=is_multi_index_rename_node(field)ifcheck:setattr(node,name,get_new_multi_index_rename_node(var,idx,val))else:patch(field)

用法示例1:

fromintelli_patchimportpatchimportastimportastunparsecode="""importpandasaspdmi=pd.MultiIndex.from_product([[1,2],[a,b]],names=[x,y])mi.levels[0].name="newname""""head=ast.parse(code)patch(head)print(astunparse.unparse(head))

输出:

importpandasaspdmi=pd.MultiIndex.from_product([[1,2],[a,b]],names=[x,y])mi=mi.set_names(newname,level=0)

用法示例2:

fromintelli_patchimportpatchimportastimportastunparsecode="""importpandasaspdclassC():deff():defg():mi.levels[0].name="newname"mi=pd.MultiIndex.from_product([[1,2],[a,b]],names=[x,y])"""head=ast.parse(code)patch(head)print(astunparse.unparse(head))

输出:

importpandasaspdclassC():deff():defg():mi=mi.set_names(newname,level=0)mi=pd.MultiIndex.from_product([[1,2],[a,b]],names=[x,y])

在用法示例2中,请注意,要被替换的代码语句多于1行,并且出现在类C的函数f中的函数g中。IntelliPatch也能处理这种情况。

可以扩展补丁脚本来处理pandas1.0.0中的所有向后兼容性。然后编写一个外部函数,遍历系统中的每一个Python文件,读取其代码,对其进行修补,然后写回到磁盘。值得注意的是,开发人员应该在提交IntelliPatch所做的更改前对其进行检查。例如,如果代码托管在git上,那么开发者应该执行一个gitdiff命令并进行检查。

影响

在Soroco,我们目前已经编写了5个IntelliPatch脚本,它们运行在10个系统上。每个脚本成功解析和修补了10个系统中的大约,行代码。就生产率而言,这项工作花费我们的一位工程师整整三天来完成。这位工程师在实现这些方案前学习了关于AST的知识。

在这5个脚本中,有一个脚本是独一无二的——一个代码清理器,而且不是一个传统的补丁。这一需求源于一个外部团体试图审查代码的大纲,而不用分享实际的逻辑和代码细节。因此,我们编写了一个清理器,它可以清理代码中的逻辑和其它关键元素,同时只保留导入、类和函数定义、文档字符、类型注解和审查所需的一些非常具体的信息。因此,AST对于构建一个代码清理器也是一个有价值的工具。

局限性

使用Python的ast包修补代码的一个问题是,它丢失了原始源代码的所有格式和注释。这可以通过使补丁更智能一点来解决。我们可以让它只解析修改过的节点,并在文件中相应的行号插入修改过的代码,而不是解析整个修补过的AST并将其写入磁盘。这些ast节点有一个lineno属性可以用来获取文件中要注入的修补过的代码的行号。

3代码质量评估

现在我们已经知道AST在编写智能补丁脚本时非常有用,在本章节,我们将解释它如何用来评估代码质量。许多IDE和代码检查器,例如PyCharm和SonarQube,使用AST来执行代码质量检查。我们可以使用AST来根据我们的需求创建我们自己的代码质量检查。下面是一些例子:

示例1:非自解释变量名

你想要你组织中的开发者在代码中使用良好的自解释的变量名。你在代码中看到的最常见的问题是使用单字符变量名,例如i、j等。下面是一个可以检查这一点的脚本。

variable_name_check.py

importastdefcheck(node):"""TakesanASTrootedatthegivennodeandchecksiftherearesinglecharactervariablenames."""#Ifitisaleafnode,thenreturn.ifnothasattr(node,"_fields"):return#Foreverychildofthenode,checkifitisavariablehavingsinglecharacter#nameandrecursivelycallcheckonit.forchild_nodeinast.iter_child_nodes(node):ifisinstance(child_node,ast.Name)andlen(child_node.id)==1:print(f"Singlecharactername{child_node.id}usedatlinenumber{child_node.lineno}")check(child_node)

用例:

fromvariable_name_checkimportcheckimportastcode="""a=1b=aprint(b)"""head=ast.parse(code)check(head)

输出:

Singlecharacternameausedatlinenumber2Singlecharacternamebusedatlinenumber3Singlecharacternameausedatlinenumber3Singlecharacternamebusedatlinenumber4示例2:未记录日志的except代码块

你想要你组织中的人员确保在捕获到异常时进行日志记录。你希望从每个except代码块调用日志模块的error或exception函数。下面是一个使用AST检查这一点的脚本。

unlogged_except_check.py

importastdefcheck(node):"""TakesanASTrootedatthegivennodeandchecksifthereareun-loggedexceptcodeblocks."""#Ifitisaleafnode,thenreturn.ifnothasattr(node,"_fields"):return#Foreverychildofthenode,checkifitisun-loggedexceptcodeblock#andrecursivelycallcheckonit.forchild_nodeinast.iter_child_nodes(node):ifisinstance(child_node,ast.ExceptHandler)andnotis_logging_present(child_node):print(f"Neithererrornorexceptionloggingispresentwithintheexceptblockstartingatlinenumber{child_node.lineno}")check(child_node)defis_logging_present(node):"""TakesanASTrootedatthegivennodeandcheckswhetherthereisanerrororexceptionloggingpresentinit."""#Ifitisaleafnode,thenreturnFalse.ifnothasattr(node,"_fields"):returnFalse#Ifitrepresentsan`error`or`exception`functioncallthenreturnTrue.if(isinstance(node,ast.Call)andisinstance(node.func,ast.Attribute)andnode.func.attrin["error","exception"]):returnTrue#Recursivelycheckingifloggingispresentinthechildrennodes.forchild_nodeinast.iter_child_nodes(node):ifis_logging_present(child_node):returnTruereturnFalse

用例:

fromunlogged_except_checkimportcheckimportastcode="""try:passexceptValueError:logging.error("Erroroccurred")try:passexceptKeyError:logging.exception("Exceptionhandled")exceptNameError:passtry:passexcept:logging.info("Infolevellogging")"""head=ast.parse(code)check(head)

输出:

Neithererrornorexceptionloggingispresentwithintheexceptblockstartingatlinenumber14Neithererrornorexceptionloggingispresentwithintheexceptblockstartingatlinenumber19

如果发现一个except代码块没有任何日志记录,可以采取进一步行动,代码质量检查器可以通过在AST中增加一个相应的节点来在代码中插入日志。

结论

AST的用途远远超过了本文的讨论范围。例如,给定系统中的文件的AST可以用来创建一个调用图。在运行时期间创建的调用图可能不会覆盖所有的代码路径。但是,使用AST静态创建的调用图会覆盖所有的代码路径,因此将是全面的。然后这个调用图可以用来创建一份人类可读的系统文档。我们在Soroco创建了这样一个功能,我们称为“LiveDoc”,我们可以改天在另外一篇文章中谈谈这个话题。

原文链接

转载请注明:http://www.zmax-alibaba.com/qgjj/137958.html

网站简介 | 发布优势 | 服务条款 | 隐私保护 | 广告合作 | 合作伙伴 | 版权申明 | 网站地图

当前时间: