掘金 后端 ( ) • 2024-04-30 15:01

highlight: zenburn

上一篇我们学习了基础环境的搭建,然后运行了一个很简单的例子: image.png

学习过面向对象语言的朋友都知道,面向对象是很难的,有着很多的特性,比如:类、对象、接口、封装、继承、多态、动态绑定、对象标识、成员变量、方法、构造器、析构器、修饰符、静态成员、类方法、实例方法、继承、组合、聚合、关联、依赖、封装、方法重载、运算符重载、动态方法解析、单继承、多继承、虚函数、纯虚函数、内聚、耦合、类型检查、类型安全、异常处理、模板、泛型等等。

那么作为一种查询“面对对象语言”的查询语言,其势必更加的复杂和晦涩难懂。 我们举个例子,看一下 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 属性定义。查询主要分为两种:

      1. Alert queries 警报查询:突出显示代码中特定位置的问题的查询。
      1. 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 ,语句必须选择两个“列”:ElementString

下面举一下 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." 一个最简单的查询结果,只包含一个一个元素一个固定文本的解释

image.png

select c, "This class extends the class " + superclass.getName() 在解释中添加父类的名称

image.png

select c, "This class extends the class $@.", superclass, superclass.getName() 添加父类的链接。字符串消息: "This class extends the class $@." —字符串文本现在包含一个占位符,该占位符将显示接下来两列的组合内容。占位符的元素: superclass,占位符的字符串文本:superclass.getBaseName() 类名。

image.png

显示警报消息时, $@ 占位符将替换为根据 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 之间的所有毕达哥拉斯三元组
    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
    
    为了简化查询,我们可以引入一个表示 1 到 10 之间的整数的类 SmallInt 。我们还可以在该类 square() 中定义一个整数谓词。以这种方式定义类和谓词可以轻松重用代码,而不必每次都重复代码。
    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
    
    image.png

编写特定语言的查询

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 Description getAge() 将人的年龄(以年为单位)返回为 int getHairColor() 将人的头发颜色返回为 string getHeight() 将人的身高(以 cm 为单位)返回为 float getLocation() 以 string 返回人的位置(north, south, east or west)

image.png

比如这样调用:

image.png

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> 子句为空。请注意,如果有几个人具有相同的最大年龄,则查询会列出所有这些人。 以下是聚合的更多示例:

Example Result 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 查询来选择所有秃头人:

image.png

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()

image.png

为合法继承人加冕

唷!村子里不再有犯罪行为——你终于可以离开村子回家了。

但是后来......在村子里的最后一晚,老国王——伟大的 “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

image.png

在下一次村民会议上,你宣布有两个活着的亲戚。

为了决定谁应该继承国王的财产,村民们仔细阅读了村章:

“王位继承人是国王最亲近的亲戚。任何有犯罪记录的人都不会被考虑。如果有多个候选人,则最年长的人是继承人。

作为你的最后一个挑战,定义一个谓词 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()