导语:在前面的文章中,我们通过一个抓小偷的例子,为大家展示了逻辑谓词、连接词和聚合操作的威力。在本文中,我们将通过一个抓纵火犯的例子来复习谓词的知识,并进一步学习类的定义和用法,以及如何覆盖父类的成员谓词。
在前面的文章中,我们通过一个抓小偷的例子,为大家展示了逻辑谓词、连接词和聚合操作的威力。在本文中,我们将通过一个抓纵火犯的例子来复习谓词的知识,并进一步学习类的定义和用法,以及如何覆盖父类的成员谓词。
简介
上一次,我们用QL破获了王冠失窃案,之后我们便获得了QL大侦探的美誉。可是,不久之后,村里又发生了一起案件:村北的庄稼地被人恶意纵火,导致地里的农作物颗粒无收,村民损失惨重。所以,我们奉命找出纵火犯。
现在,我们除了手头上的村民信息之外,还掌握了一条重要线索:村南和村北的村民之间交恶已久,所以,纵火犯很可能就是住在村南的村民。
在破案过程中,我们将进一步介绍如何在QL代码中定义和使用谓词和类。有了它们,不仅可以让我们编写的查询逻辑变得更加易于理解,同时,还有助于简化我们的侦查工作。
缩小包围圈
现在,怀疑对象已经被锁定为一个特定的村民群体,即那些生活在村庄南部的村民。因此,我们可以定义一个新的谓词southern,用于遴选住在村南的村民,这样的话,就不必在所有查询这些村民的代码中加入GetLocation()=“south”这一条件了。谓词southern的定义如下所示:
predicate southern(Person p) { p.getLocation() = "south" }
使用谓词southern(p)时,我们需要提供一个参数p,以便让谓词检查p是否满足条件p.getLocation() = “south”,也就是住在村南。
好了,根据上面的例子,我们再次复习一下谓词的定义和分类。我们知道,谓词分为两类:一类是带有返回值的谓词,另一类是没有返回值的谓词。在定义谓词时,首先要注意的是,谓词的名称必须以小写字母开头。另外,上面的这个谓词属于没有返回结果的谓词,定义这种类型的谓词时,需要使用关键字predicate;不过,当需要定义带有返回结果的谓词的时候,需要把这里的关键字predicate替换为返回结果的数据类型。这时,还需要引入了一个新的参数来保存返回结果——特殊变量result。例如,int getAge() {result = …}便是定义了一个返回整型数据的谓词。
现在,我们就可以利用上面定义的谓词来找出所有住在村南的居民了,具体代码如下所示:
/* 谓词southern的定义如上所示 */
from Person p where southern(p) select p
运行结果如下所示:
如您所见,利用谓词的好处是,不仅使得我们的代码的逻辑更加清晰,同时,也能提高编写代码的效率。对于上面的查询代码,from子句表示要考察所有村民(Person p),然后,在where子句中加入了一个限制条件:住在村南的居民(southern(p))。
实际上,除了利用上面的查询来找出考察对象之外,我们还可以自定义一个Southerner类,用它来找出我们的考察对象,也就是住在村南的居民,具体代码如下所示:
class Southerner extends Person { Southerner() { southern(this) } }
对于QL语言来说,可以用类来表示一个逻辑属性:当一个值满足该属性时,它就是类的成员。这意味着一个值可以属于多个类,这其实不难理解,举例来说,3既属于“整数”类,也属于“奇数”类,同时属于“质数”类,等等。
在上面的类的定义中,表达式southern(this)定义了这个类所表示的逻辑属性,我们称这个谓词为这个类的特征谓词。需要注意的是,这个表达式中使用了一个特殊变量this,就这里来耍,该变量表示一个Person类型的值,也就是一个村民;如果this满足southern(this)这一限制条件,那么,this代表的村民就属于Southerner类,也就是居住在村南的村民。
对于熟悉面向对象编程语言的读者来说,会发现特征谓词跟构造函数非常类似。不过,特征谓词并非构造函数,实际上它是一个逻辑属性,并且不创建任何对象。
在QL语言中,我们通常需要根据现有的类(超集)来定义新的类(子集)。 在我们的例子中,Southerner是村民中的一个特殊群体,所以,我们说Southerner(住在村南的村民)类继承自Person(村民)类,换句话说,Southerner是Person的一个子集。
借助于这个类,在列出所有住在村南的居民的时候,相应的代码会变得更加简洁:
from Southerner s select s
第一句是声明变量,就是住在村南的村民,然后,没有附加任何条件就直接列出这些变量了。完整的代码如下所示:
运行结果如下所示:
通过上面的例子,您可能已经注意到,有些谓词是跟在某些变量后面的,例如p.getAge();而有些则是以参数的形式传递变量,例如southern(p)。这是因为,getAge()是一个定义在类Person中的一个成员谓词(类似于成员函数),也就是说,它是一个只能用于该类中的成员变量的谓词。在定义类时,我们也可定义自己的成员谓词,这一点将在下面看到。相反,谓词southern是单独定义的,不属于任何类。实际上,我们还可以将多个成员谓词串起来完成一系列的操作,例如,p. getage ().sqrt(),这里首先获取村民p的年龄,然后计算年龄的平方根——用起来是不是特别方便啊!
出行管制
在这里,我们还要考虑另一个因素:发生王冠失窃案后,村子里实施了出行管制。案发之前,村民是可以在村子里自由走动的,因此,谓词isAllowedIn(string region) 是适用于任何村民和任何区域的。举例来说,下面的查询代码将会列出所有村民,因为他们都可以去村北:
from Person p where p.isAllowedIn("north") select p
完整的代码如下所示:
运行结果如下所示:
然而,在发生盗窃案之后,村民们变得非常警惕,所以,不再允许10岁以下的儿童离开居住地,例如,村北的孩子不能到村南、村东、村西去玩了。这就意味着,谓词isAllowedIn(string region) 已经不再适用于所有村民和所有区域,所以,当p表示一个孩子时,我们应该临时覆盖原来的谓词isAllowedIn(string region)。
为此,需要首先定义一个类Child,用以表示10岁以下所有村民。然后,在类Child类中,我们重新定义成员谓词isAllowedIn(string region),以使孩子们只能在自己的地盘上走动。这一限制可以通过region = this.getLocation()来进行表示。好了,现在大家终于明白了吧?上面所说的覆盖,实际上就是重新定义父类中的成员谓词。
class Child extends Person { /* the characteristic predicate */ Child() { this.getAge() < 10 } /* a member predicate */ override predicate isAllowedIn(string region) { region = this.getLocation() } }
现在,当我们将谓词isAllowedIn(string region)应用于表示村民的变量p上的时候,如果变量p的类型不是Child,也就是这个村民不是一个孩子的时候,则使用该谓词原来的定义;但是如果变量p的类型为Child,也就是说这个变量表示的村民是一个孩子,那么,该谓词将会使用Child类中的新定义,从而覆盖原来的代码。
根据现有的线索,我们知道纵火犯住在村南,所以,他们必定是被允许到村北走动的。为此,我们可以编写一个查询来找出可能的嫌疑犯。同时,我们还可以扩展select子句来列出嫌疑人的年龄。具体代码如下所示:
运行结果如下所示:
通过年龄一栏我们就可以清楚地看到:所有的孩子都被排除在了嫌疑人名单之外。
好了,接下来我们还需要进一步收集更多线索,以便找出真正的纵火犯!
揪出真凶
为了找出真凶,我们继续在村北寻找线索,幸运的是,我们这次找到了目击证人!就在火灾发生后,住在田地旁边的农民看见两个人仓皇逃跑了。虽然他们只看到了嫌疑人的背影,却注意到他们都是秃头。
太好了,这是一个非常有用的线索——这样以来,我们就可以通过QL查询来查找所有秃头的人了,具体代码如下所示
from Person p where not exists (string c | p.getHairColor() = c) select p
如上所示,这里使用了排除法来找出秃头的村民,也就是发色无法与现有村民发色相匹配的人,即not exists (string c | p.getHairColor() = c)——因为秃子根本没有头发,哪里来的发色呀!当然,也许有读者觉得每次找秃子都需要输入一长串代码的做法,不仅费时费力,而且容易出错,那有没有更好的办法呢?别急,我们可以把这些代码封装到一个谓词中,之后只需要调用这个谓词就行了,具体代码如下所示:
predicate bald(Person p) { not exists (string c | p.getHairColor() = c) }
当村民p是一个秃子时,属性bald(p)便成立,因此,之前用来判断村民是否为秃子时的查询代码可以简化为:
from Person p where bald(p) select p
根据前面的定义,我们知道谓词bald的参数的类型为Person,所以,我们也可以使用Southerner类型的变量作为其参数,因为Southerner类型是Person类型的一个子类型。但是,我们却不能用整型变量作为其参数,这会导致语法错误。
好了,利用上面介绍的知识,现在可以编写一个查询,来查找住在村南的且可以进入村北的秃头村民,具体代码如下所示:
import tutorial predicate southern(Person p) { p.getLocation() = "south" } class Southerner extends Person { /* the characteristic predicate */ Southerner() { southern(this) } } class Child extends Person { /* the characteristic predicate */ Child() { this.getAge() < 10 } /* a member predicate */ override predicate isAllowedIn(string region) { region = this.getLocation() } } predicate bald(Person p) { not exists (string c | p.getHairColor() = c) } from Southerner s where s.isAllowedIn("north") and bald(s) select s
上面的代码的运行结果如下所示:
纵火犯终于落网了!
小结
在本文中,我们以破获纵火案为例,回顾了谓词和类的定义和使用,同时,还介绍了如何覆盖成员谓词。至此,QL语言的威力给村民们留下来深刻的印象,在下一篇文章中,我们将受村民之托,帮他们寻找合法的王位继承人。
备注:本系列文章乃本人在学习CodeQL平台过程中所做的笔记,希望能够对大家有点滴帮助——若果真如此的话,本人将备感荣幸。
参考资料:
https://help.semmle.com/
转自:https://www.4hou.com/posts/J7k9
转载请注明:jinglingshu的博客 » 代码分析平台CodeQL学习手记(六)