highlight: zenburn
上一篇我们学习了基础环境的搭建,然后运行了一个很简单的例子:
学习过面向对象语言的朋友都知道,面向对象是很难的,有着很多的特性,比如:类、对象、接口、封装、继承、多态、动态绑定、对象标识、成员变量、方法、构造器、析构器、修饰符、静态成员、类方法、实例方法、继承、组合、聚合、关联、依赖、封装、方法重载、运算符重载、动态方法解析、单继承、多继承、虚函数、纯虚函数、内聚、耦合、类型检查、类型安全、异常处理、模板、泛型等等。
那么作为一种查询“面对对象语言”的查询语言,其势必更加的复杂和晦涩难懂。 我们举个例子,看一下 SDK 中关于 Method 的定义:
/** A method is a particular kind of callable. */
class Method extends Callable, @method {
/** Holds if this method (directly) overrides the specified callable. */
predicate overrides(Method m) { overrides(this, m) }
/**
* Holds if this method either overrides `m`, or `m` is the
* source declaration of this method (and not equal to it).
*/
predicate overridesOrInstantiates(Method m) {
this.overrides(m)
or
this.getSourceDeclaration() = m and this != m
}
/** Gets a method (directly or transitively) overridden by this method. */
Method getAnOverride() { this.overrides+(result) }
/** Gets the source declaration of a method overridden by this method. */
SrcMethod getASourceOverriddenMethod() {
exists(Method m | this.overrides(m) and result = m.getSourceDeclaration())
}
...
...
...
全都是没见过的新关键字,那么为了学会 CodeQL 这门语言,就很有必要像当初学 java 一样,学习一下什么是 “class”、“predicate”、“overrides” 等等这些词代表什么。
基本的查询结构
这个就是基本的查询结构:
/**
*
* 查询的描述:元数据 metadata
*
*/
import /* ... 导入 CodeQL 的 SDK 库 ... */
/* ... 定义 CodeQL 类和谓词(可选) ... */
from /* ... 变量声明 ... */
where /* ... 逻辑表达式 ... */
select /* ... 选择表达式 ... */
查询元数据 metadata 用于在将自定义查询添加到 GitHub 存储库或用于分析时识别自定义查询。元数据提供有关查询用途的信息,还指定如何解释和显示查询结果。官方介绍:https://github.com/github/codeql/blob/main/docs/query-metadata-style-guide.md 如果您使用 CodeQL CLI 分析数据库,则查询元数据必须包含 @kind 。如果使用 VS Code 的 CodeQL 扩展运行查询,则元数据不是必需的。但是,如果希望将结果显示为“警报”或“路径”,则必须指定正确的 @kind 属性。
-
import :每个查询通常包含一个或多个 import 语句,这些语句定义要导入到查询中的库或模块。库和模块提供了一种将相关类型、谓词和其他模块组合在一起的方法。就是导入官方写的库函数什么的。还有一些库包含常用的谓词、类型以及与不同分析相关的其他模块,包括数据流、控制流和污点跟踪。为了计算路径图,路径查询要求将数据流库导入查询文件。
-
from:声明查询中使用的变量。每个声明的格式为
from {type} {变量名}, {type} {变量名}, ...
-
where: 就是通过逻辑表达式去筛选想要的结果
-
select: 指定如何显示得到的结果,select 子句的有效结构由元数据中指定的 @kind 属性定义。查询主要分为两种:
-
- Alert queries 警报查询:突出显示代码中特定位置的问题的查询。
-
- Path queries 路径查询:描述代码中源和接收器之间的信息流的查询。
警报查询 ( @kind problem ) 的 Select 子句由两个“列”组成,结构如下:
select element, string
, element :由查询标识的代码元素,用于定义警报的显示位置。string :一条消息,其中还可以包含链接和占位符,说明生成警报的原因。路径查询 ( @kind path-problem ) 的 Select 子句旨在显示警报以及关联路径图的源和接收器。
还有一些其他的查询: 诊断查询 ( @kind diagnostic ) 和摘要指标查询 ( @kind metric 和 @tags summary ) 的 Select 子句具有不同的要求。
-
元数据的属性
Property Value Description@description
<text>
一个句子或简短的段落,用于描述查询的目的以及结果有用或重要的原因。描述以纯文本形式编写,并使用单引号 ( '
) 将代码元素括起来。
@id
<text>
由小写字母或数字组成的单词序列,以 /
或 -
分隔,用于标识查询并对其进行分类。每个查询必须具有唯一的 ID。为确保这一点,对每个 ID 使用固定结构可能会有所帮助。例如,标准 CodeQL 查询采用以下格式: <language>/<brief-description>
.
@kind
problem
path-problem
标识查询是警报 ( @kind problem
) 还是路径 ( @kind path-problem
)。
@name
<text>
定义查询标签的语句。该名称以纯文本形式编写,并使用单引号 ( '
) 将代码元素括起来。
@tags
correctness
maintainability
readability
security
这些标记将查询分组到广泛的类别中,以便于搜索和识别它们。
@precision
low
medium
high
very-high
指示真阳性(而不是假阳性结果)的查询结果的百分比。这与 @problem.severity
属性一起决定了结果在 GitHub 上的显示方式。
@problem.severity
error
warning
recommendation
定义由非安全查询生成的任何警报的严重性级别。这与 @precision
属性一起决定了结果在 GitHub 上的显示方式。
@security-severity
<score>
定义 0.0 到 10.0 之间的 @tags security
严重性级别.
接下来展示一个实例:
/**
* @name 查询所有的方法
* @description 简单查询 WebGoat 中所使用的所有的方法函数
* @kind problem
* @problem.severity recommendation
* @precision very-high
* @id java/webgoat-all-function
* @tags readability
* security
* @security-severity 0.1
*/
import java
from Method m
select m, "一个方法"
如果你写的查询过于复杂,或者你想教别人写 QL 查询,你还可以为你的 QL 查询写帮助文件,查询帮助文件使用自定义 XML 格式编写,并存储在 .qhelp 扩展名为的文件中。查询帮助文件必须与它们所描述的查询具有相同的基本名称,并且必须位于同一目录中。详情见:查询帮助文件 — CodeQL --- Query help files — CodeQL (github.com)
构造提示型查询
您可以通过修改查询 select 语句来控制分析结果在源代码中的显示方式。
查询结果中包含的信息由 select 语句控制。让查询结果清晰易懂,以便其他用户理解,是非常重要的。在 VS Code 的 CodeQL 扩展中编写自己的查询时,对可以选择的内容没有限制。但是,如果要使用查询创建代码扫描警报或使用 CodeQL CLI 生成有效的分析结果,则需要以所需的格式生成 select 语句报告结果。还必须确保查询定义了相应的元数据属性。
警报查询必须在其元数据中 @kind problem 定义属性。在最基本的形式中 select ,语句必须选择两个“列”:Element 和 String
下面举一下 select 的例子,我们查询一个类,然后再查询他的父类。
/**
* @kind problem
* @id java/webgoat-all-class
* @problem.severity recommendation
*/
import java
from Class c, Class superclass
where superclass = c.getASupertype()
select c, "This class extends another class."
getASupertype() 方法就是返回直接父类。
select c, "This class extends another class."
一个最简单的查询结果,只包含一个一个元素一个固定文本的解释
select c, "This class extends the class " + superclass.getName()
在解释中添加父类的名称
select c, "This class extends the class $@.", superclass, superclass.getName()
添加父类的链接。字符串消息: "This class extends the class $@." —字符串文本现在包含一个占位符,该占位符将显示接下来两列的组合内容。占位符的元素: superclass,占位符的字符串文本:superclass.getBaseName() 类名。
显示警报消息时, $@ 占位符将替换为根据 select 语句定义的第三列和第四列的内容创建的链接,请注意,某些超类(如 Object )不会出现在数据库中,因为它们内置于 Java 语言中。单击这些链接将不起作用。
如果在说明文本中多次使用 $@ 占位符标记,则第 N(从0开始) 个占位符将替换为列 2N+2 和 2N+3 组成(从0开始)的链接。如果附加列的对数多于占位符标记数,则忽略尾随列。相反,如果附加列的对数少于占位符标记数,则尾随标记将被视为普通文本,而不是占位符标记。
练习基础语法
- 练习 1: 编写一个查询,返回字符串 "lgtm" 的长度。
- 练习 2: 编写一个查询,计算 3^5 和 245.6 中比较小的那个数的正弦值。
- 练习 3: 编写一个查询,该查询返回与布尔值
false
相反的查询。 - 练习 4: 编写一个查询,用于计算 2017 年 6 月 10 日至 9 月 28 日之间的天数。
- 练习 5: 查询计算 1 到 10 之间的所有毕达哥拉斯三元组
请尝试编写一下代码,然后对照答案吧。
- 答案 1: 编写一个查询,返回字符串 "lgtm" 的长度。
from string s where s = "lgtm" select s.length().toString(), "Test" 当然也可以直接: select "lgtm".length().toString()
- 答案 2: 编写一个查询,计算 3^5 和 245.6 中比较小的那个数的正弦值。
from float x, float y where x = 3.pow(5) and y = 245.6 select x.minimum(y).sin()
- 答案 3: 编写一个查询,该查询返回与布尔值
false
相反的查询。from boolean b where b = false select b.booleanNot()
- 答案 4 :编写一个查询,用于计算 2017 年 6 月 10 日至 9 月 28 日之间的天数。
from date start, date end where start = "10/06/2017".toDate() and end = "28/09/2017".toDate() select start.daysTo(end)
- 答案 5 :查询计算 1 到 10 之间的所有毕达哥拉斯三元组
为了简化查询,我们可以引入一个表示 1 到 10 之间的整数的类 SmallInt 。我们还可以在该类 square() 中定义一个整数谓词。以这种方式定义类和谓词可以轻松重用代码,而不必每次都重复代码。from int x, int y, int z where x in [1..10] and y in [1..10] and z in [1..10] and x*x + y*y = z*z select x, y, z
class SmallInt extends int { SmallInt() { this in [1..10] } int square() { result = this*this } } from SmallInt x, SmallInt y, SmallInt z where x.square() + y.square() = z.square() select x, y, z
编写特定语言的查询
import python
from Function f
where count(f.getAnArg()) > 7
select f
查询方法、方法的参数个数大于 7
import javascript
from Comment c
where c.getText().regexpMatch("(?si).*\\bTODO\\b.*")
select c
查询注释、注释中可以匹配到 TODO
import java
from Parameter p
where not exists(p.getAnAccess())
select p
该 from 子句定义了一个表示 Java 参数的变量 p 。该 where 子句通过将参数限制为未访问的参数 p 来查找未使用的参数。最后,该 select 子句列出了这些参数。
侦探小游戏
这是一个 QL 侦探谜题,向您展示如何在 QL 中编写查询。
抓到小偷
CodeQL SDK 里附带了一个抓小偷的游戏,你可以通过 import tutorial 来使用这个库。
有一个隐藏在山上的小村庄。村庄分为北、南、东、西四个部分,中心矗立着一座黑暗而神秘的城堡......在城堡内,锁在最高的塔楼里,躺着国王珍贵的金冠。一天晚上,发生了一起可怕的罪行。一个小偷闯入塔楼并偷走了王冠!你知道小偷一定住在村子里,因为没有人知道王冠。经过一些专业的侦探工作,你会得到一份村子里所有人的名单和他们的一些个人详细信息。这里的每个人都有以下几个信息:Name | Age | Hair color | Height | Location 。 可悲的是,你仍然不知道谁偷走了王冠,所以你在村子里走来走去寻找线索。村民们的行为非常可疑,你确信他们有关于小偷的信息。他们拒绝直接与你分享他们的知识,但他们不情愿地同意回答问题。他们仍然不是很健谈,只用“是”或“否”来回答问题。你开始问一些有创意的问题,并记下答案,以便你以后可以将它们与你的信息进行比较:
Question Answer 1 小偷身高超过150厘米吗? yes 2 小偷有金发吗? no 3 小偷是秃头吗? no 4 小偷还不到30岁吗? no 5 小偷住在城堡的东边吗? yes 6 小偷的头发是黑色的或者棕色的? yes 7 小偷身高超过180cm,身高低于190cm吗? no 8 小偷是村里年纪最大的人吗? no 9 小偷是村里最高的人吗? no 10 小偷比一般村民矮吗? yes 11 小偷是村子东部最年长的人吗? yes有太多的信息需要手动搜索,所以你决定使用你新获得的QL技能来帮助你进行调查......
在这个库中,官方定义了许多 QL 谓词,以帮助你从表中提取数据。QL 谓词是一个小型查询,用于表达各种数据之间的关系并描述它们的一些属性。在这种情况下,谓词会为您提供有关一个人的信息,例如他们的身高或年龄。你可以直接调用这些谓词或者说叫函数。
Predicate DescriptiongetAge()
将人的年龄(以年为单位)返回为 int
getHairColor()
将人的头发颜色返回为 string
getHeight()
将人的身高(以 cm 为单位)返回为 float
getLocation()
以 string 返回人的位置(north, south, east or west)
比如这样调用:
OK 你可以开始的抓小偷之旅了,如果没有头绪,那么也可以回来看看,接下来将介绍一些基础的语法。
# 连接谓词 and
where t.getAge() > 30 and t.getHairColor() = "brown"
# 或者 or
where t.getHairColor() = "brown" or t.getHairColor() = "black"
# 取反 not
where not t.getLocation() = "north"
# 区分优先级,组合长句子。如果没有括号,连接词 and 优先 or 于 。
where t.getAge() > 30
and (t.getHairColor() = "brown" or t.getHairColor() = "black")
and not t.getLocation() = "north"
谓词并不总是只返回一个值。例如,如果一个人 p 的黑发正在变灰, p.getHairColor() 则将返回两个值:黑色和灰色。
如果小偷是秃头怎么办?在这种情况下,小偷没有头发,所以 getHairColor() 谓词根本不会返回任何结果!
如果你知道小偷绝对不是秃头,那么一定有一种颜色与小偷的头发颜色相匹配。在 QL 中表示这一点的一种方法是引入一个新的变量 c 类型 string ,并选择 t 与值 t.getHairColor() 匹配的 c 变量。
from Person t, string c
where t.getHairColor() = c
select t
请注意,我们只是暂时引入了变量 c ,在 select 子句中根本不需要它。在这种情况下,最好使用 exists :
from Person t
where exists(string c | t.getHairColor() = c)
select t
如果你熟悉逻辑,你可能会注意到 exists in QL 对应于逻辑中的存在量词。QL 还有一个通用量词 forall(vars | formula 1 | formula 2): 意思是 vars 同时满足表达式1 和 表达式 2,它在逻辑上等同于 not exists(vars | formula 1 | not formula 2): 意思是,嗯,有点绕啊,我也不懂啊。理解不了啊
聚合查询
# 您可以使用 max 聚合来查找村中年龄最大的人的年龄:
where t.getAge() = max(int i | exists(Person p | p.getAge() = i) | i)
这种通用的聚合语法很长,而且很不方便。在大多数情况下,可以省略聚合的某些部分。一个特别有用的 QL 功能是有序聚合。这允许您使用 order by 对表达式进行排序。例如,如果使用有序聚合,则选择最年长的村民会变得简单得多。
select max(Person p | | p order by p.getAge())
有序聚合会考虑每个人 p ,然后选择具有最大年龄的人。在这种情况下,对要考虑的人没有限制,因此该 <logical formula>
子句为空。请注意,如果有几个人具有相同的最大年龄,则查询会列出所有这些人。
以下是聚合的更多示例:
min(Person p | p.getLocation() = "east" | p order by p.getHeight())
村东最矮的人
count(Person p | p.getLocation() = "south" | p)
村南的人数
avg(Person p | | p.getHeight())
村民平均身高
sum(Person p | p.getHairColor() = "brown" | p.getAge())
所有棕色头发的村民的年龄总和
最终的答案:
import tutorial
from Person t
where
/* 1 */ t.getHeight() > 150 and
/* 2 */ not t.getHairColor() = "blond" and
/* 3 */ exists (string c | t.getHairColor() = c) and
/* 4 */ not t.getAge() < 30 and
/* 5 */ t.getLocation() = "east" and
/* 6 */ (t.getHairColor() = "black" or t.getHairColor() = "brown") and
/* 7 */ not (t.getHeight() > 180 and t.getHeight() < 190) and
/* 8 */ exists(Person p | p.getAge() > t.getAge()) and
/* 9 */ not t = max(Person p | | p order by p.getHeight()) and
/* 10 */ t.getHeight() < avg(float i | exists(Person p | p.getHeight() = i) | i) and
/* 11 */ t = max(Person p | p.getLocation() = "east" | p order by p.getAge())
select "The thief is " + t + "!"
抓到纵火犯
就在你成功找到小偷并将金冠归还给城堡时,又发生了一起可怕的罪行。一大早,几个人在村子北边的田地里放火,把庄稼都烧光了!这一次,您有一些额外的信息。村子的南北之间有很强的竞争,你知道罪犯住在南方。
这一次,你只需要考虑一个特定的村民群体,即那些住在村庄南部的村民。与其将 getLocation() = "south" 写入每个判断语句,不如定义一个新的谓词 isSouthern (就像一个函数):
predicate isSouthern(Person p) {
p.getLocation() = "south"
}
您还可以使用返回值的类型定义谓词。在这种情况下,关键字 predicate 将替换为结果的类型。这就像引入一个新参数,即特殊变量 result 。例如:
int getAge(){
result = ...
}
您现在可以使用以下方式列出所有南方人:
import tutorial
predicate isSouthern(Person p) {
p.getLocation() = "south"
}
from Person fire
where isSouthern(fire)
select fire
这已经是简化逻辑的好方法,但我们可以更有效率。目前,查询会查看每个 Person p ,然后限制为满足 isSouthern(p) 的 。相反,我们可以定义一个新类 Southerner ,精确地包含我们想要考虑的人。
import tutorial
class Southerner extends Person {
Southerner() { isSouthern(this) }
}
predicate isSouthern(Person p) {
p.getLocation() = "south"
}
from Southerner fire
select fire
QL 中的类表示逻辑属性:当值满足该属性时,它是该类的成员。这意味着一个值可以存在于许多类中 - 位于特定类中并不会阻止它出现在其他类中。
表达式 Southerner() { isSouthern(this) }
定义由类表示的逻辑属性,称为其特征谓词。它使用一个特殊变量 this ,并指示这个 Person。
如果您熟悉面向对象的编程语言,您可能会倾向于将特征谓词视为构造函数。但是,事实并非如此,它是一个逻辑属性,不会创建任何对象。
使用这个类,您现在可以简单地列出居住在南方的所有人:
from Southerner s
select s
您可能已经注意到,某些谓词被附加,例如 p.getAge() ,而其他谓词则没有,例如 isSouthern(p) 。这是因为 getAge() 是成员谓词,即仅适用于类成员的谓词。您可以在类中定义这样的成员谓词。在本例中, getAge() 在类 Person 中定义。相反, isSouthern 它是单独定义的,不在任何类中。成员谓词特别有用,因为您可以轻松地将它们链接在一起。例如, p.getAge().sqrt() 首先获取该数字的 p 年龄,然后计算该数字的平方根。
您要考虑的另一个因素是皇冠被盗后实施的旅行限制。最初,村民在村内的出行地点没有限制。因此,谓词 isAllowedIn(string region) 适用于任何人和任何地区。以下查询列出了所有村民,因为他们都可以向北旅行:
from Person p where p.isAllowedIn("north") select p
然而,在最近的盗窃案发生后,村民们更加担心潜伏在村子周围的犯罪分子,他们不再允许10岁以下的儿童离开村庄。 这意味着 isAllowedIn(string region) 不再适用于所有人员和所有村庄,因此如果 p 是儿童类,则应暂时覆盖原始谓词。 首先定义一个包含所有 10 岁以下村民的类 Child 。然后,您可以重新定义为 isAllowedIn(string region) 成员谓词, Child 以仅允许儿童类在其自己的村庄内移动。这表示为 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() } }
现在尝试定义一个 p,然后调用 isAllowedIn(string region) .如果 p 不是儿童,则使用原始定义,但如果 p 是儿童,则新的谓词定义将覆盖原始定义。 你知道点火者住在南方,他们一定能够前往北方。编写查询以查找可能的嫌疑人。您还可以扩展该 select 条款以列出嫌疑人的年龄。这样,您可以清楚地看到所有孩子都已被排除在列表之外。
import tutorial
predicate isSouthern(Person p) { p.getLocation() = "south" }
class Southerner extends Person {
/* the characteristic predicate */
Southerner() { isSouthern(this) }
}
class Child extends Person {
/* the characteristic predicate */
Child() { this.getAge() < 10 }
/* a member predicate */
override predicate isAllowedIn(string region) { region = this.getLocation() }
}
from Southerner s
where s.isAllowedIn("north")
select s, s.getAge()
当然了,你也可以用排除法, not exists 就是排除的意思
from Southerner southerner
where not exists(Child child | southerner = child)
select southerner, southerner.getAge()
你问北方人是否有更多关于纵火犯的信息。幸运的是,你有一个证人!住在田地旁边的农民看到两个人在火灾发生后逃跑了。他只看到他们的头顶,注意到他们都是秃头。 这是一个非常有用的线索。请记住,您编写了一个 QL 查询来选择所有秃头人:
from Southerner southerner
where not exists(string s | southerner.getHairColor() = s)
select southerner, southerner.getAge()
为了避免每次要选择秃头的人时都必须键入 not exists (string c | p.getHairColor() = c) ,您可以改为定义另一个新谓词 isBald 。
predicate isBald(Person p) {
not exists (string c | p.getHairColor() = c)
}
因此可以将上一个查询替换为:
from Person p
where isBald(p)
select p
OK,现在你可以抓到那两个纵火犯了
import tutorial
class Southerner extends Person {
Southerner() { isSouthern(this) }
}
class Child extends Person{
Child(){ this.getAge() < 10}
override predicate isAllowedIn(string region) {
region = this.getLocation()
}
}
predicate isSouthern(Person p) {
p.getLocation() = "south"
}
predicate isBald(Person p) {
not exists(string s | p.getHairColor() = s)
}
from Southerner southerner
where isBald(southerner) and southerner.isAllowedIn("north")
select southerner, southerner.getAge()
为合法继承人加冕
唷!村子里不再有犯罪行为——你终于可以离开村子回家了。
但是后来......在村子里的最后一晚,老国王——伟大的 “King Basil” 国王——在睡梦中去世了,到处都是混乱!
国王从未结婚,也没有孩子,所以没有人知道谁应该继承国王的城堡和财富。很快,许多村民声称他们在某种程度上是国王家族的后裔,他们是真正的继承人。人们争吵和争吵,情况似乎毫无希望。
最终,你决定留在村子里解决争论,找到真正的王位继承人。
你想知道村子里是否有人真的与国王有关系。乍一看,这似乎是一项艰巨的任务,但您自信地开始工作。你现在已经很了解村民了,你有一份村里所有父母和他们的孩子的名单。
要了解更多关于国王和他的家人的信息,您可以进入城堡并找到一些古老的家谱。您还可以将这些关系包含在您的数据库中,以查看国王家族中是否有人还活着。
以下谓词可用于帮助您访问数据:
Predicate Description parentOf(Person p) 返回 p 的父类
例如,您可以列出所有人 p 及其父母:
from Person p
select parentOf(p) + " is a parent of " + p
有太多的信息需要手动搜索,所以你写了一个QL查询来帮助你找到国王的继承人。 我们知道国王自己没有孩子,但也许他有兄弟姐妹。编写查询以找出:
from Person p
where parentOf(p) = parentOf("King Basil") and
not p = "King Basil"
select p
他确实有兄弟姐妹!但是你需要检查他们中的任何一个是否还活着......下面是您可能需要的另一个谓词:isDeceased()
判断该人是否已亡故,使用这个谓词来查看国王的兄弟姐妹是否还活着。
from Person p
where parentOf(p) = parentOf("King Basil") and not p = "King Basil" and p.isDeceased()
select p + " is already dead"
不幸的是,巴西尔国王的兄弟姐妹都 G 了。是时候进一步调查了。定义一个返回该人的孩子的谓词 childOf() 可能会有所帮助。为此, parentOf() 可以在 childOf() 中使用。
Person childOf(Person p){
parentOf(result) = p
}
试着写一个查询,看看国王的兄弟姐妹是否有孩子:
from Person p
where parentOf(p) = parentOf("King Basil") and
not p = "King Basil"
select childOf(p)
查询不返回任何结果,因此它们没有孩子。但也许罗勒国王有一个还活着的有孩子的堂兄,或者叔伯、姨舅、爷爷等等等等
这变得越来越复杂。理想情况下,您希望定义一个列出 的所有 p 亲戚的谓词 relativeOf(Person p) 。 你怎么能这样做呢?
它有助于思考相对的精确定义。一个可能的定义是,如果两个人有共同的祖先,他们就是有血缘关系的。
您可以引入一个谓词,该谓词 ancestorOf(Person p) 列出了 p 的所有祖先。祖先是指 p 的父级或者父级的父级或者父级的父级的父级,依此类推。不幸的是,这导致了无穷无尽的父母名单。您无法编写无限 QL 查询,因此必须有一种更简单的方法。
啊哈,你有个主意!你可以说祖先要么是父母,要么是你已经知道是祖先的人的父母。
您可以按如下方式将其转换为 QL:
Person ancestorOf(Person p) {
result = parentOf(p) or
result = parentOf(ancestorOf(p))
}
如您所见,您已在 ancestorOf() 其自己的定义中使用了自己。这是递归的一个示例。
这种递归,其中相同的操作(在本例 parentOf() 中)被多次应用,在 QL 中非常常见,称为操作的传递闭包。有两个特殊符号 + * ,在处理传递闭包时非常有用:
parentOf+(p)
将谓词应用于 p 一次或多次。这相当于 ancestorOf(p)
parentOf*(p)
将谓词应用于 p 零次或更多次,因此它返回 p 本身 OR p 的祖先。
尝试使用这种新符号来定义谓词 relativeOf() ,并用它来列出国王的所有在世亲属。
下面是一种定义 relativeOf() 方法:
Person relativeOf(Person p) {
parentOf*(result) = parentOf*(p)
}
不要忘记使用谓语 isDeceased() 来寻找还活着的亲戚。
import tutorial
Person relativeOf(Person p){
parentOf*(result) = parentOf*(p)
}
from Person p
where p = relativeOf("King Basil") and not p.isDeceased()
select p
在下一次村民会议上,你宣布有两个活着的亲戚。
为了决定谁应该继承国王的财产,村民们仔细阅读了村章:
“王位继承人是国王最亲近的亲戚。任何有犯罪记录的人都不会被考虑。如果有多个候选人,则最年长的人是继承人。
作为你的最后一个挑战,定义一个谓词 hasCriminalRecord ,以便使得 p 在你之前揭露的任何罪犯(在“抓到小偷”和“抓到纵火犯”教程中) hasCriminalRecord(p) 中成立。
import tutorial
Person relativeOf(Person p){
parentOf*(result) = parentOf*(p)
}
predicate hasCriminalRecord(Person p) {
p = "Hester" or p = "Hugh" or p = "Charlie"
}
from Person p
where p = relativeOf("King Basil") and not p.isDeceased() and not hasCriminalRecord(p)
select p
祝贺!你找到了王位继承人,恢复了村庄的和平。但是,您还不必离开村民。关于村规,还有几个问题,你可以通过写QL查询来为村民回答:
- 哪个村民是王位的下一个继承人?你能写一个谓词来确定剩下的村民与新君主的关系有多密切吗?
- 如果多个村民与君主有相同的关系,您将如何使用 QL 查询选择最年长的候选人?
你也可以尝试写更多你自己的QL查询,以找到关于村民的有趣事实。您可以自由调查任何您喜欢的内容,但这里有一些建议:
- 村里最常见的发色是什么?在哪个地区?
- 哪个村民的孩子最多?谁的后代最多?
- 村里每个地区有多少人居住?
- 所有村民都和他们的父母住在村子的同一个地区吗?
- 看看村子里有没有时间旅行者!(提示:寻找“不可能”的家庭关系。)
我自己尝试写个答案吧。
哪个村民是王位的下一个继承人?你能写一个谓词来确定剩下的村民与新君主的关系有多密切吗?
import tutorial
Person relativeOf(Person p){
parentOf*(result) = parentOf*(p)
}
Person childOf(Person p){
parentOf(result) = p
}
predicate hasCriminalRecord(Person p) {
p = "Hester" or p = "Hugh" or p = "Charlie"
}
Person inheritor(Person king){
// 确认下一任每一任国王的方法, 我也不知道如何设置优先级,先这样吧,
(result = childOf(king) and not result.isDeceased()) or // 国王的儿子犯法那叫犯法吗?
(result = relativeOf(king) and not result.isDeceased() and not hasCriminalRecord(result))
}
from Person king
where king = "Clara"
select max(Person p | p = inheritor(king) | p order by p.getAge())
- 如果多个村民与君主有相同的关系,您将如何使用 QL 查询选择最年长的候选人?
select max(Person p | p = inheritor(king) | p order by p.getAge())
你也可以尝试写更多你自己的QL查询,以找到关于村民的有趣事实。您可以自由调查任何您喜欢的内容,但这里有一些建议:
- 村里最常见的发色是什么?每个地区呢?
import tutorial
class HairColor extends string {
HairColor(){
// 在特征函数里, this 可以理解为集合
exists(Person p | this = p.getHairColor())
}
int countColor(){
// 在成员函数中,this 可以理解为某一个确定的实例
result = count(Person p | p.getHairColor() = this | p)
}
}
from HairColor maxhc
where not exists(HairColor hc | hc.countColor() > maxhc.countColor())
select maxhc, maxhc.countColor()
每个地区的头发颜色最多的数量
import tutorial
class HairColor extends string {
HairColor(){
// 在特征函数里, this 可以理解为集合
exists(Person p | this = p.getHairColor())
}
int countColorWithLocation(Location location){
result = count(Person p | p.getHairColor() = this and p.getLocation() = location)
}
}
class Location extends string{
Location(){
exists(Person p | this = p.getLocation())
}
}
from HairColor maxhc, Location location
where not exists(HairColor hc | hc.countColorWithLocation(location) > maxhc.countColorWithLocation(location))
select location, maxhc, maxhc.countColorWithLocation(location)
- 哪个村民的孩子最多?谁的后代最多?
import tutorial
Person childOf(Person p) {
parentOf(result) = p
}
int countChildNumber(Person p){
result = count(Person child | child = childOf(p) | child)
}
from Person p
where countChildNumber(p) = max(Person tmp| |countChildNumber(tmp))
select p, countChildNumber(p)
import tutorial
Person descendantOf(Person p) {
result = parentOf+(p)
}
int countDescendantNumber(Person p){
result = count(Person child | child = descendantOf(p) | child)
}
from Person p
where countDescendantNumber(p) = max(Person tmp| |countDescendantNumber(tmp))
select p, countDescendantNumber(p)
- 村里每个地区有多少人居住?
import tutorial
class Location extends string{
Location(){
exists(Person p | this = p.getLocation())
}
int countNumber(){
result = count(Person p | p.getLocation() = this | p)
}
}
from Location p
select p, p.countNumber()
- 所有村民都和他们的父母住在村子的同一个地区吗?
import tutorial
from Person parent, Person child
where parent = parentOf(child) and
not exists( | child.getLocation() != parent.getLocation() | child.getLocation() = parent.getLocation()) and
child.getLocation() != parent.getLocation()
select child
- 看看村子里有没有时间旅行者!(提示:寻找“不可能”的家庭关系。)
import tutorial
Person descendantOf(Person p) {
result = parentOf+(p)
}
from Person timeTraveler, Person p
where exists(|descendantOf(timeTraveler) = p and timeTraveler.getAge() > p.getAge() )
select timeTraveler, timeTraveler.getAge(), p, p.getAge()