CodeQL 文档

找到小偷

扮演侦探的角色,在这个虚构的村庄里找到小偷。你将学习如何使用 QL 中的逻辑连接词、量词和聚合。

简介

在群山深处隐藏着一个村庄。村庄分为四个部分——北、南、东、西,村庄中心矗立着一座阴暗神秘的城堡……城堡里,最高塔里锁着一个宝贵的金皇冠。一天晚上,发生了一起可怕的罪行。一个小偷闯入塔里,偷走了皇冠!

你知道小偷一定住在村子里,因为只有村民知道皇冠。经过一番专家级侦探工作,你获得了村里所有人的姓名和部分个人资料。

姓名 年龄 头发颜色 身高 位置

不幸的是,你仍然不知道是谁偷走了皇冠,所以你四处走动寻找线索。村民们表现得非常可疑,你确信他们知道关于小偷的信息。他们拒绝直接告诉你,但他们勉强同意回答问题。他们仍然不太爱说话,而且**只用“是”或“否”来回答问题**。

你开始问一些有创意的问题,并记下答案,以便以后与你的信息进行比较。

  问题 答案
小偷的身高超过 150 厘米吗?
小偷的头发是金色的吗?
小偷是秃头的吗?
小偷的年龄小于 30 岁吗?
小偷住在城堡的东边吗?
小偷的头发是黑色或棕色吗?
小偷的身高在 180 厘米到 190 厘米之间吗?
小偷是村里年龄最大的人吗?
小偷是村里个子最高的人吗?
小偷的身高低于村民的平均身高吗?
小偷是村庄东部年龄最大的人吗?

信息太多,无法手动搜索,所以你决定使用你新获得的 QL 技能来帮助你调查……

注意

你可以在 GitHub Codespaces 中使用 CodeQL 模板(测试版)来试用这些教程中的 QL 概念和与编程语言无关的示例。该模板包含一个引导式介绍,介绍了如何使用 QL,并使你能够轻松上手。

当你准备好对实际代码库运行 CodeQL 查询时,需要在 Visual Studio Code 中安装 CodeQL 扩展。有关说明,请参阅 GitHub 文档中的 为 Visual Studio Code 安装 CodeQL

QL 库

我们定义了一系列 QL 谓词,以帮助你从表格中提取数据。QL 谓词是一个小型查询,它表达了各种数据之间的关系,并描述了它们的一些属性。在这种情况下,谓词会提供有关一个人的信息,例如他们的身高或年龄。

谓词 描述
getAge() 返回一个人的年龄(以年为单位),数据类型为 int
getHairColor() 返回一个人的头发颜色,数据类型为 string
getHeight() 返回一个人的身高(以厘米为单位),数据类型为 float
getLocation() 返回一个人的住址(北、南、东、西),数据类型为 string

我们将这些谓词存储在 QL 库 tutorial.qll 中。若要访问此库,请在查询控制台中键入 import tutorial

库便于存储常用谓词。这可以让你不必每次需要谓词时都重新定义。相反,你可以直接 import 库并直接使用谓词。导入库后,你可以通过追加谓词将任何谓词应用于表达式。

例如,t.getHeight()getHeight() 应用于 t 并返回 t 的身高。

逻辑连接词

使用 逻辑连接词,你可以编写更复杂的查询,这些查询可以组合不同的信息片段。

例如,如果你知道小偷的年龄超过 30 岁,而且头发是棕色的,你可以使用以下 where 子句来链接两个谓词。

where t.getAge() > 30 and t.getHairColor() = "brown"

注意

谓词 getHairColor() 返回一个 string,所以我们需要在结果 "brown" 周围加上引号。

如果小偷没有住在城堡的北边,你可以使用

where not t.getLocation() = "north"

如果小偷的头发是棕色或黑色,你可以使用

where t.getHairColor() = "brown" or t.getHairColor() = "black"

你还可以将这些连接词组合成更长的语句

where t.getAge() > 30
  and (t.getHairColor() = "brown" or t.getHairColor() = "black")
  and not t.getLocation() = "north"

注意

我们在 or 子句周围加了括号,以确保查询按预期进行评估。如果没有括号,连接词 and 优先于 or

谓词并不总是返回一个值。例如,如果一个人 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 引入一个临时变量 c,类型为 string,并且只有在至少存在一个满足 t.getHairColor() = cstring c 时才成立。

注意

如果你熟悉逻辑,你可能会注意到 QL 中的 exists 对应于逻辑中的存在 量词。QL 也有一个全称量词 forall(vars | formula 1 | formula 2),在逻辑上等价于 not exists(vars | formula 1 | not formula 2)

真正的调查

你现在已经准备好追踪小偷了!使用上面的示例,编写一个查询,以查找满足前八个问题的答案的人。

  问题 答案
1 小偷的身高超过 150 厘米吗?
2 小偷的头发是金色的吗?
3 小偷是秃头的吗?
4 小偷的年龄小于 30 岁吗?
5 小偷住在城堡的东边吗?
6 小偷的头发是黑色或棕色吗?
7 小偷的身高在 180 厘米到 190 厘米之间吗?
8 小偷是村里年龄最大的人吗?

提示

  1. 不要忘记 import tutorial
  2. 分别将每个问题翻译成 QL。如果你卡住了,请查看上面的示例。
  3. 对于问题 3,请记住,秃头的人没有头发颜色。
  4. 对于问题 8,请注意,如果一个人不是年龄最大的,那么至少存在一个比他年龄更大的人。
  5. 使用逻辑连接词组合这些条件,以得到以下形式的查询
import tutorial

from Person t
where <condition 1> and
  not <condition 2> and
  ...
select t

完成后,你将获得一个可能的嫌疑人列表。这些人中一定有一个是小偷!

查看你的答案

你越来越接近解开谜团了!不幸的是,你仍然有一个很长的嫌疑人列表……要找出你的嫌疑人中谁是小偷,你必须收集更多信息并在下一步细化你的查询。

更高级的查询

如果你想找到村里年龄最大、最小、个子最高或最矮的人怎么办?如前一主题所述,你可以使用 exists 来实现这一点。但是,在 QL 中还有一种更有效的方法可以做到这一点,使用 maxmin 之类的函数。这些是 聚合 的示例。

一般来说,聚合函数是对多个数据进行操作并返回单个值作为输出的函数。常见的聚合函数包括 countmaxminavg(平均值)和 sum。使用聚合函数的一般方式是

<aggregate>(<variable declarations> | <logical formula> | <expression>)

例如,你可以使用 max 聚合函数来查找村庄中最年长的人的年龄

max(int i | exists(Person p | p.getAge() = i) | i)

该聚合函数会考虑所有整数 i,将 i 限制为与村庄中的人的年龄匹配的值,然后返回最大的匹配整数。

但是,如何在实际查询中使用它呢?

如果小偷是村庄中最年长的人,那么你知道小偷的年龄等于村民的最大年龄

from Person t
where t.getAge() = max(int i | exists(Person p | p.getAge() = i) | i)
select t

这种通用的聚合语法相当冗长且不方便。在大多数情况下,你可以省略聚合函数的某些部分。一个特别有用的 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()) 所有棕色头发村民的年龄总和

抓捕罪犯

现在你可以将剩余的问题翻译成 QL

  问题 答案
9 小偷是村里个子最高的人吗?
10 小偷的身高低于村民的平均身高吗?
11 小偷是村庄东部年龄最大的人吗?

你找到小偷了吗?

查看答案

进一步阅读


答案

在这些答案中,我们使用 /**/ 来标记查询的不同部分。任何被 /**/ 包围的文本都不会作为 QL 代码的一部分进行评估,而是被视为注释

练习 1

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

练习 2

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 + "!"
  • ©GitHub, Inc.
  • 条款
  • 隐私