CodeQL 学习笔记【3】QL 语法基础 - 1 (谓词、查询、类型)

390 阅读25分钟

OK,上一篇我们照葫芦画瓢学习了 CodeQL 的查询编写的方法,但是却知其然不知其所以然,那么这篇文章,我们就来详细介绍一下每个关键词都是什么意思吧。

关于 QL 语言

QL 是 CodeQL 的基础,是用于分析代码的强大查询语言。

QL 是一种声明性的、面向对象的查询语言,它经过优化,可以有效地分析分层数据结构,特别是表示软件工件的数据库。(具体啥是分层数据结构,咱也不懂,应该是 CodeQL 将代码转换成了 某种数据结构,方便查找。)

举个例子,最常用的数据库模型是关系型数据库模型,而 SQL(结构化查询语言)是关系型数据库最常用的查询语言。同理,CodeQL 可以将代码转换为某种数据库模型,然后 QL 就是这种数据库模型的查询语言。

QL 查询语言的目的是提供一个编程平台,您可以在其中查询关于代码的各种问题。查询通常引用相关的数据库实体,并指定结果必须满足的各种条件(称为谓词)。查询评估涉及检查这些谓词并生成结果。一个好的查询语言及其实现的一些理想属性包括:

  • 声明性规范 - 声明结果必须满足哪些属性,而不是像 java 那样提供计算过程,计算出某种结果。
  • 表现力 - 一种强大的查询语言,允许您编写复杂的查询。这使得该语言具有广泛的适用性。
  • 高效执行 - 查询可能很复杂,数据库可能非常大,因此查询语言实现高效处理和执行查询至关重要。

QL 的属性

QL 的语法类似于 SQL,但 QL 的语义基于 Datalog,QL 中的所有操作都是逻辑操作。此外,QL 继承了 Datalog 的递归谓词,并增加了对聚合的支持,使复杂的查询变得简洁明了。例如,一个包含父子关系的数据库。如果我们想找到一个人的后代数量,通常我们会:

  • 找到给定人的所有后代,即孩子或孩子的后代。
  • 计算上一步找到的后代数。

我们使用递归来查找给定人员的所有后代,并使用聚合来计算后代的数量。由于语言的声明性质,可以在不添加任何过程细节的情况下将这些步骤转换为最终查询。QL 代码如下所示:

Person getADescendant(Person p) {
  result = p.getAChild() or
  result = getADescendant(p.getAChild())
}

int getNumberOfDescendants(Person p) {
  result = count(getADescendant(p))
}

QL 和面向对象

面向对象是 QL 的一个重要特征。

它增加了模块化,支持信息隐藏,并允许代码重用。

QL 在不影响其逻辑基础的情况下提供了所有这些好处。

这是通过定义一个简单的对象模型来实现的,其中类被建模为谓词,继承被建模为隐含。为一些语言,比如 java、go 什么的,提供一些已经写好的类和继承。

QL 和通用编程语言

以下是通用编程语言和 QL 之间的一些突出的概念和功能差异:

  • QL 没有任何命令性功能,例如变量赋值或文件系统操作。
  • QL 操作在元组集上,一个查询行为,可以被看作为 ”对一个集合的操作序列“。
  • QL 基于集合的语义使得处理集合变得非常自然,而不必担心存储、索引和遍历它们。
  • 在面向对象的编程语言中,实例化类涉及通过分配物理内存来保存该类的实例的状态。在 QL 中,类只是描述现有值集的逻辑属性。

谓词 Predicates

谓词描述了 QL 程序的逻辑关系。

百度百科:谓语(Predicate)是对主语动作状态或特征的陈述或说明,指出"做什么(what to do)" , "是什么(what is this)"。

严格来说,谓词的计算结果为一组元组。例如:

predicate isCountry(string country) {
  country = "Germany"
  or
  country = "Belgium"
  or
  country = "France"
}

predicate hasCapital(string country, string capital) {
  country = "Belgium" and capital = "Brussels"
  or
  country = "Germany" and capital = "Berlin"
  or
  country = "France" and capital = "Paris"
}

谓词 isCountry 是一元组的集合 {("Belgium"),("Germany"),("France")} ,而 hasCapital 谓词是二元组的集合 {("Belgium","Brussels"),("Germany","Berlin"),("France","Paris")} 。这些谓词的 arity(数量,或者说大小) 分别是 1 和 2。

通常,谓词中的所有元组都具有相同数量的元素。谓词的 arity 是元素的数量.

QL 中有许多内置谓词。您可以在任何查询中使用它们,而无需导入任何其他模块。除了这些内置谓词之外,您还可以定义自己的谓词.

定义谓词

定义谓词时,应指定:

  • 关键字 predicate (对于没有结果的谓词)或结果的类型(对于有结果的谓词)。
  • 谓词的名称。这是一个以小写字母开头的标识符。
  • 谓词的参数(如果有),用逗号分隔。对于每个参数,指定参数变量的参数类型和标识符。
  • 谓语主体本身。这是一个用大括号括起来的逻辑公式。 抽象谓词或外部谓词没有主体。若要定义此类谓词,请改用分号 ( ; ) 结束谓词定义。

没有返回值的谓词

这些谓词定义以关键字 predicate 开头。如果某个值满足正文中的逻辑属性,则谓词将适用于该值。

predicate isSmall(int i) {
  i in [1 .. 9]
}

如果 i 是一个 int ,那么 isSmall(i) 成立的条件是 i 为小于 10 的正整数.

带有返回值的谓词

您可以通过将关键字 predicate 替换为返回值的类型来定义具有结果的谓词。这引入了特殊变量 result 。

int getSuccessor(int i) {
  result = i + 1 and
  i in [1 .. 9]
}

如果 i 是小于 10 的正整数,则谓词的结果是 i 的后继数。

请注意,您可以像对谓词的任何其他参数一样使用 result 。您可以用任何您喜欢的方式表达 result 与其他变量之间的关系  。例如,假如已经有了一个返回x 的父亲的谓词 getAParentOf(Person x) ,可以定义一个“反向”谓词,如下所示:

Person getAChildOf(Person p) {
  p = getAParentOf(result)
}

谓词也可以对其参数的每个值有多个结果(或根本没有结果)。例如:

string getANeighbor(string country) {
  country = "France" and result = "Belgium"
  or
  country = "France" and result = "Germany"
  or
  country = "Germany" and result = "Austria"
  or
  country = "Germany" and result = "Belgium"
}

在这种情况下:

  • 调用谓词 getANeighbor("Germany") 返回两个结果: "Austria" 和 "Belgium" 。
  • 调用谓词 getANeighbor("Belgium") 不返回任何结果,因为 getANeighbor 没有为 "Belgium" 定义  result  。

递归谓词

QL 中的谓词可以是递归的。这意味着它直接或间接地依赖于自身。

例如,您可以使用递归来优化上述示例。就目前而言,getANeighbor 中 定义的关系是不对称的,即如果 x 是 y 的邻居,那么 y 就是 x 的邻居。捕获此值的一种简单方法是以递归方式调用此谓词,如下所示:

string getANeighbor(string country) {
  country = "France" and result = "Belgium"
  or
  country = "France" and result = "Germany"
  or
  country = "Germany" and result = "Austria"
  or
  country = "Germany" and result = "Belgium"
  or
  country = getANeighbor(result)
}

现在 getANeighbor("Belgium") 返回结果,即 "France" 和 "Germany" 。

谓词的种类

谓词有三种,即非成员谓词、成员谓词和特征谓词。

非成员谓词是在类外部定义的,也就是说,它们不是任何类的成员。

下面是一个示例,显示了每种类型的谓词:

int getSuccessor(int i) {  // 1. 非成员谓词
  result = i + 1 and
  i in [1 .. 9]
}

class FavoriteNumbers extends int {
  FavoriteNumbers() {  // 2. 特征谓词
    this = 1 or
    this = 4 or
    this = 9
  }

  string getName() {   // 3. FavoriteNumbers 类的成员谓词
    this = 1 and result = "one"
    or
    this = 4 and result = "four"
    or
    this = 9 and result = "nine"
  }
}

绑定行为

必须能够在有限的时间内计算谓词,因此它所描述的集合通常不允许是无限的。换句话说,一个谓词只能包含有限数量的元组。

当 QL 编译器可以证明谓词为无限数值的变量时,它会报告错误。

以下是无限谓词的几个示例:

/*
编译错误:
错误:“i”未绑定到值。
错误:“result”未绑定到值。
错误:表达式“i*4”未绑定到值。
*/
int multiplyBy4(int i) {
  result = i * 4
}

/*
编译错误:
错误:“str”未绑定到值。
错误:表达式“str.length()”未绑定到值。
*/
predicate shortString(string str) {
  str.length() < 10
}

multiplyBy4 中  ,参数 i 被声明为 int ,它是一个无限类型。它被用于二进制运算 * ,它不绑定其操作数。 result 从一开始就是未绑定的,并且保持未绑定状态,因为它用于与 i * 4 的相等性检查,而 i * 4 也是未绑定的。

在 shortString 中 str 保持未绑定状态,因为它是用无限类型 string 声明的,并且内置函数 length() 不绑定它。

绑定集

有时您可能希望定义一个“无限谓词”,因为您只打算在一组受限的参数上使用它。在这种情况下,可以使用批注指定显式绑定集 bindingset 。此批注对任何类型的谓词都有效。

bindingset[i]
int multiplyBy4(int i) {
  result = i * 4
}

from int i
where i in [1 .. 10]
select multiplyBy4(i)

虽然 multiplyBy4 是无限谓词,但上面的 QL 查询是合法的。它首先使用 bindingset 注释来声明谓词 multiplyBy4 将是有限的,前提是 i 它绑定到有限数量的值。然后,它在限制为 [1 .. 10]  范围 的上下文 i 中使用谓词。

也可以为一个谓词声明多个绑定集。这可以通过添加多个绑定集注释来完成,例如:

bindingset[x] bindingset[y]
predicate plusOne(int x, int y) {
  x + 1 = y
}

from int x, int y
where y = 42 and plusOne(x, y)
select x, y

以这种方式指定的多个绑定集彼此独立。上面的例子意味着:

  • 如果 x 被绑定,则 x 和 y 被绑定。
  • 如果 y 被绑定,则 x 和 y 被绑定。

也就是说, bindingset[x] bindingset[y] 它声明至少一个 x or y 必须绑定,与 bindingset[x, y] 不同,后者声明两者 x y 必须绑定。

当您想要声明具有具有多个输入参数的结果的谓词时,后者可能很有用。例如,以下谓词采用字符串 str 并将其截断为最大 len 字符长度:

bindingset[str, len]
string truncate(string str, int len) {
  if str.length() > len
  then result = str.prefix(len)
  else result = str
}

然后,您可以在 select 子句中使用它,例如:

select truncate("hello world", 5)

数据库谓词

查询的每个数据库都包含表示值之间关系的表。这些表(“数据库谓词”)的处理方式与 QL 中的其他谓词相同。

例如,如果数据库包含人员表,则可以写入 persons(x, firstName, _, age) 约束 x 、 firstName 和 age 作为该表中的第一列、第二列和第四行。

唯一的区别是您不能在 QL 中定义数据库谓词。它们由基础数据库定义。因此,可用的数据库谓词因要查询的数据库而异。

查询 Queries

查询就是 QL 程序的输出。共有两种查询模式。

  • 一个文件中定义的 select 子句(如果有)。
  • 一个文件的谓词命名空间中的任何查询谓词。也就是说,它们可以在模块本身中定义,也可以从其他模块导入。

我们通常也将整个 QL 程序称为查询。

选择子句

编写查询模块时,可以包含以下形式的 select 子句(通常位于文件末尾):

from /* ... 变量定义 ... */
where /* ... 逻辑表达式 ... */
select /* ... 选择表达式 ... */

from 和 where 部件是可选的。

除了在 “选择表达式”中编写基本表达式外,您还可以对表达式进行拓展:

  • as 关键字,后跟名称。这将为结果列提供“标签”,并允许在后续选择表达式中使用它们。
  • order by 关键字,后跟结果列的名称,以及可选的关键字 asc 或 desc .这决定了结果的显示顺序。
from int x, int y
where x = 3 and y in [0 .. 2]
select x, y, x * y as product, "product: " + product

image.png 您也可以在 select 子句的末尾添加 order by y desc 。现在,结果根据 y 列中的值按降序排序:

image.png

查询谓词

查询谓词是带有 query 批注的非成员谓词。它返回谓词计算出的所有元组。

query int getProduct(int x, int y) {
  x = 3 and
  y in [0 .. 2] and
  result = x * y
}

image.png

编写查询谓词而不是 select 子句的一个好处是,您也可以在代码的其他部分调用该谓词。例如,可以在类的正文中调用 getProduct :

class MultipleOfThree extends int {
  MultipleOfThree() { this = getProduct(_, _) }
}

相比之下,select 子句就像一个匿名谓词,所以以后不能调用它。

在调试代码时向谓词添加 query 批注也很有帮助。这样,就可以显式地看到谓词计算到的元组集。

类型 Type

QL 是一种静态类型语言,因此每个变量都必须具有声明的类型。

类型是一组值。例如,类型 int 是整数的集合。请注意,一个特定的值可以属于多个集合,这意味着一个特定的值可以有多个类型。

QL 中的类型有基元类型、类、字符类型、类域类型、代数数据类型、类型联合和数据库类型。

基元类型

这些类型内置于 QL 中,并且始终在全局命名空间中可用,与要查询的数据库无关。

boolean 只包含 truefalse
float 64 位浮点数
int 32 位补码整数
string 16 bit 的字符串
date 日期类型

QL 具有一系列在基元类型上定义的内置操作。例如, 1.toString() 是整数常量 1 的字符串表示形式。

您可以在 QL 中定义自己的类型。一种方法是定义一个类。

类提供了一种重用和构建代码的简单方法

  • 将相关值组合在一起。
  • 根据这些值定义成员谓词。
  • 定义覆盖成员谓词的子类。

QL 中的类不会“创建”新对象,它只是表示一个逻辑属性。如果一个值满足该逻辑属性,则该值位于特定类中。

 定义类

要定义一个类,您可以编写:

  • 关键字 class
  • 类的名称。这是一个以大写字母开头的标识符。
  • 通过 extends / instanceof 派生类
  • 类的主体,用大括号括起来。
class OneTwoThree extends int {
  OneTwoThree() { // 特征谓词
    this = 1 or this = 2 or this = 3
  }

  string getAString() { // 成员谓词
    result = "One, two or three: " + this.toString()
  }

  predicate isEven() { // 成员谓词
    this = 2
  }
}

这定义了一个类 OneTwoThree ,其中包含值 1 、 2 和 3 。

OneTwoThree 继承了 int ,即它是 int 的子类型。QL 中的类必须继承至少一个超类型。使用 extends 关键字引用的超类型称为类的基类型。类的值包含在超类型的交集内(即,它们位于类域类型中)。类从其基类型继承所有成员谓词。

一个类可以扩展多种类型。类可以扩展最终类型(或类型的最终别名),类还可以专门化其他类型,而无需通过 instanceof 扩展类接口

类的主体

类的主体可以包含:

  • 特征谓词声明。
  • 任意数量的成员谓词声明。
  • 任意数量的字段声明。

定义类时,该类还会从其超类型继承所有非私有成员谓词和字段。

根据它们是否是最终的,您可以覆盖或隐藏这些谓词和字段,以赋予它们更具体的定义。

特征谓词

这些是在类的主体中定义的谓词。它们是使用变量 this 来限制类中可能的值的逻辑属性。

成员谓词

这些是仅适用于特定类的成员的谓词。您可以对值调用成员谓词。例如,您可以使用上述类中的成员谓词:

1.(OneTwoThree).getAString()  // 1 先进行类型转换 然后调用成员谓词

此调用返回结果 "One, two or three: 1" 。

表达式 (OneTwoThree) 是强制转换。它确保 1 具有类型 OneTwoThree 而不仅仅是 int .因此,它有权访问成员谓词 getAString() 。

成员谓词特别有用,因为您可以将它们链接在一起。例如,您可以使用 toUpperCase() 定义的 string 内置函数:

1.(OneTwoThree).getAString().toUpperCase()

此调用返回 "ONE, TWO OR THREE: 1" 。

特征谓词和成员谓词通常可以使用变量 this 。此变量始终引用类的成员,在本例中是属于类 OneTwoThree 的值。在特征谓词中,变量 this 约束类中的值。在成员谓词中, this 其作用方式与谓词的任何其他参数相同。

字段

这些变量是在类的主体中声明的变量。一个类的主体中可以有任意数量的字段声明(即变量声明)。可以在类内的谓词声明中使用这些变量。与变量 this 非常相似,字段必须约束在特征谓词中。

class SmallInt extends int {
  SmallInt() { this = [1 .. 10] }
}

class DivisibleInt extends SmallInt {
  SmallInt divisor;   // 字段的声明
  DivisibleInt() { this % divisor = 0 }

  SmallInt getADivisor() { result = divisor }
}

from DivisibleInt i
select i, i.getADivisor()

在此示例中,声明 SmallInt divisor 引入了一个字段 divisor ,将其约束在特征谓词中,然后在成员谓词 getADivisor 的声明中使用它。这类似于通过在 from 零件中声明变量来在 select 子句中引入变量。

具体类

上面示例中的类都是具体类。它们是通过限制较大类型的值来定义的。具体类中的值恰恰是超类型交集中的那些值,这些值也满足类的特征谓词。

抽象类

用 abstract 注释的类称为抽象类,也是对较大类型中值的限制。但是,抽象类被定义为其子类的并集。特别是,要使值位于抽象类中,它必须满足类本身的特征谓词和子类的特征谓词。请注意,在此上下文中,最终扩展不被视为子类。

如果要将多个现有类组合到一个通用名称下,则抽象类非常有用。然后,可以在所有这些类上定义成员谓词。您还可以扩展预定义的抽象类:例如,如果导入包含抽象类的库,则可以向其添加更多子类。

abstract class SqlExpr extends Expr {
  ...
}

现在定义各种子类 — 每种数据库管理系统一个子类。例如,您可以定义一个子类 class PostgresSqlExpr extends SqlExpr ,其中包含传递给执行数据库查询的某些 Postgres API 的表达式。您可以为 MySQL 和其他数据库管理系统定义类似的子类。

抽象类 SqlExpr 引用所有这些不同的表达式。如果以后要添加对另一个数据库系统的支持,只需将新的子类添加到 SqlExpr ;无需更新依赖它的查询。

抽象类 SqlExpr 引用所有这些不同的表达式。如果以后要添加对另一个数据库系统的支持,只需将新的子类添加到 SqlExpr ;无需更新依赖它的查询。

抽象类 SqlExpr 引用所有这些不同的表达式。如果以后要添加对另一个数据库系统的支持,只需将新的子类添加到 SqlExpr ;无需更新依赖它的查询。

将新子类添加到现有抽象类时必须小心。添加子类不是一个孤立的更改,它还扩展了抽象类,因为它是其子类的并集。

覆盖成员谓词

这个覆盖可使真覆盖啊,强制覆盖,就算你实例一个父类,然后压根没实例子类,但是子类也会直接给你覆盖了,这一点跟 java 什么的完全不一样

如果类从非最终超类型继承成员谓词,则可以覆盖继承的定义。为此,请定义与继承的谓词具有相同名称和名称的成员谓词,并添加 override 批注。如果要优化谓词,以便为子类中的值提供更具体的结果,这将非常有用。

class OneTwo extends OneTwoThree {
  OneTwo() {
    this = 1 or this = 2
  }

  override string getAString() {
    result = "One or two: " + this.toString()
  }
}

成员谓词 getAString() 覆盖  OneTwoThree 的原始 getAString() 定义。

from OneTwoThree o
select o, o.getAString()
ogetAString() 结果
1One or two: 1
2One or two: 2
3One, two or three: 3

这里就跟传统的 java 不一样了,父类对象竟然能自动调用子类的函数???或者说子类能强行覆盖父类的函数。

image.png

在 QL 中,与其他面向对象语言不同,相同类型的不同子类型不需要严格分割。例如,您可以定义 的另一个 OneTwoThree 子类,它与 OneTwo 部分重叠  :

class TwoThree extends OneTwoThree {
  TwoThree() {
    this = 2 or this = 3
  }

  override string getAString() {
    result = "Two or three: " + this.toString()
  }
}

image.png

???这就离谱啊。

多重继承

一个类可以扩展多种类型。在这种情况下,它继承了所有这些类型。

例如,使用上一节中的定义:

class Two extends OneTwo, TwoThree {}

类 Two 中的任何值都必须满足 OneTwo 的逻辑属性和 TwoThree 的逻辑属性。所以这里,类 Two 包含一个值,即 2。

它继承自 OneTwo 和 TwoThree 的成员谓词。它还(间接)继承自 OneTwoThree 和 int 。

如果子类继承了同一谓词名称的多个定义,则它必须重写这些定义以避免歧义。在这种情况下,超级表达式通常很有用。

最终扩展

类可以扩展最终类型或类型的最终别名。在这种情况下,它将继承这些超类型的所有成员谓词和字段的最终版本。无法覆盖通过最终扩展继承的成员谓词,但可以对其进行重影处理。

例如,从第一个示例扩展类:

final class FinalOneTwoThree = OneTwoThree;

class OneTwoFinalExtension extends FinalOneTwoThree {
  OneTwoFinalExtension() {
    this = 1 or this = 2
  }

  string getAString() {
    result = "One or two: " + this.toString()
  }
}

成员谓词  getAString()  遮蔽了OneTwoThree 的原始 getAString() 定义。

与覆盖不同,最终扩展保持扩展类型不变:

from OneTwoFinalExtension o
select o, o.getAString()

image.png

非扩展子类型

除了扩展基类型之外,类还可以声明 instanceof 与其他类型的关系。将类声明为 instanceof Foo 大致等同于 this instanceof Foo。主要区别在于,您可以在Bar 上 使用 super 关键字调用 Foo 的方法,并且可以获得更好的优化。

class Foo extends int {
  Foo() { this in [1 .. 10] }

  string fooMethod() { result = "foo" }
}

class Bar instanceof Foo {
  string toString() { result = super.fooMethod() }
}

在此示例中,来源自 Foo 的特征谓词 fooMethod 也适用于 Bar 。但是,其并未在 中 Bar 公开, 因此查询 select any(Bar b).fooMethod() 会导致编译时错误。

至关重要的是,超类型的实例不是基类型。这意味着这些超类型不参与覆盖,并且此类超类型的任何字段都不是新类的一部分。当涉及复杂的类层次结构时,这对方法解析有影响。下面的示例演示了这一点。

 class Interface extends int {
   Interface() { this in [1 .. 10] }
   string foo() { result = "" }
}

 class Foo extends int {
   Foo() { this in [1 .. 5] }
   string foo() { result = "foo" }
 }

 class Bar extends Interface instanceof Foo {
   override string foo() { result = "bar" }
 }

这个时候 Bar::foo 没有覆盖 Foo::foo, 仅仅只覆盖了 Interface::foo ,就是说select any(Foo f).foo()返回 fooselect any(Interface f).foo() 返回 bar.

字符类型和类域类型

您不能直接引用这些类型,但 QL 中的每个类都隐式定义了字符类型和类域类型。(这些是相当微妙的概念,在实际的查询编写中并不经常出现。

QL 类的字符类型是满足类的特征谓词的值集。它是域类型的子集。对于具体类,当且仅当值属于字符类型时,该值才属于该类。对于抽象类,除了属于字符类型之外,值还必须属于至少一个子类。

QL 类的域类型是其所有超类型的字符类型的交集,也就是说,如果一个值属于每个超类型,则该值属于该域类型。它作为类的特征谓词中的类型 this 出现。

代数数据类型

代数数据类型的语法被认为是实验性的,可能会发生变化。但是,它们出现在标准 QL 库中,因此以下部分应有助于您理解这些示例。

代数数据类型是用户定义类型的另一种形式,使用关键字 newtype 声明。

代数数据类型用于创建既不是基元值也不是数据库实体的新值。例如,在分析通过程序的数据流时对流节点进行建模。

代数数据类型由许多相互不相交的分支组成,每个分支定义一个分支类型。代数数据类型本身是所有分支类型的并集。分支可以有参数和正文。为满足参数类型和正文的每组值生成分支类型的新值。

这样做的好处是每个分支可以具有不同的结构。例如,如果要定义一个包含值(如 Call )或为空的“选项类型”,则可以按如下方式编写:

newtype OptionCall = SomeCall(Call c) or NoCall()

这意味着对于程序中的每一个 Call ,都会产生一个不同的 SomeCall 值。这也意味着产生了独特的 NoCall 价值。

定义代数数据类型

若要定义代数数据类型,请使用以下常规语法:

newtype <TypeName> = <branches>

分支定义具有以下形式:

<BranchName>(<arguments>) { <body> }
  • 类型名称和分支名称必须是以大写字母开头的标识符。通常,它们以 T .
  • 代数数据类型的不同分支用 or 分隔。
  • 分支的参数(如果有)是用逗号分隔的变量声明。
  • 分支的主体是谓词主体。您可以省略分支正文,在这种情况下,它默认为 any() 。请注意,分支主体是完全计算的,因此它们必须是有限的。它们应保持较小以获得良好的性能。

例如,以下代数数据类型有三个分支:

newtype T =
  Type1(A a, B b) { body(a, b) }
  or
  Type2(C c)
  or
  Type3()

使用代数数据类型的标准模式

代数数据类型与类不同。具体而言,代数数据类型没有 toString() 成员谓词,因此不能在 select 子句中使用它们。

类通常用于扩展代数数据类型(并提供 toString() 谓词)。在标准 QL 语言库中,这通常按如下方式完成:

  • 定义一个类,该类 A 扩展代数数据类型并选择性地声明抽象谓词。
  • 对于每个分支类型,定义一个扩展 A 和 分支类型的类 B ,并为 中 A 的任何抽象谓词提供定义。
  • 使用 private 注释代数数据类型,并将类保留为公共类。

例如,适用于 C# 的 CodeQL 数据流库中的以下代码片段定义了用于处理受污染或未受污染的值的类。在这种情况下,扩展数据库类型是没有意义的 TaintType 。它是污点分析的一部分,而不是底层程序,因此扩展新类型(即 TTaintType ):

private newtype TTaintType =
  TExactValue()
  or
  TTaintedValue()

/** Describes how data is tainted. */
class TaintType extends TTaintType {
  string toString() {
    this = TExactValue() and result = "exact"
    or
    this = TTaintedValue() and result = "tainted"
  }
}

/** A taint type where the data is untainted. */
class Untainted extends TaintType, TExactValue {
}

/** A taint type where the data is tainted. */
class Tainted extends TaintType, TTaintedValue {
}

类型并集

类型联合是用户定义的类型,使用关键字 class 声明。语法类似于类型别名,但右侧有两个或多个类型表达式。

类型并集用于创建现有代数数据类型的受限子集,方法是显式选择该数据类型的分支子集并将它们绑定到新类型。还支持数据库类型的类型联合。

可以使用类型并集为代数数据类型中的分支子集指定名称。在某些情况下,在整个代数数据类型上使用类型并集可以避免谓词中的虚假递归。例如,以下结构是合法的:

newtype InitialValueSource =
  ExplicitInitialization(VarDecl v) { exists(v.getInitializer()) } or
  ParameterPassing(Call c, int pos) { exists(c.getParameter(pos)) } or
  UnknownInitialGarbage(VarDecl v) { not exists(DefiniteInitialization di | v = target(di)) }

class DefiniteInitialization = ParameterPassing or ExplicitInitialization;

VarDecl target(DefiniteInitialization di) {
  di = ExplicitInitialization(result) or
  exists(Call c, int pos | di = ParameterPassing(c, pos) and
                            result = c.getCallee().getFormalArg(pos))
}

然而,在类扩展中限制 InitialValueSource 的类似实现是无效的。如果我们将 DefiniteInitialization 实现为类扩展,它将触发InitialValueSource 的类型测试。. 这会导致非法递归 DefiniteInitialization -> InitialValueSource -> UnknownInitialGarbage -> ¬DefiniteInitialization 由于 UnknownInitialGarbage 依赖  DefiniteInitialization:

// THIS WON'T WORK: The implicit type check for InitialValueSource involves an illegal recursion
// DefiniteInitialization -> InitialValueSource -> UnknownInitialGarbage -> ¬DefiniteInitialization!
class DefiniteInitialization extends InitialValueSource {
  DefiniteInitialization() {
    this instanceof ParameterPassing or this instanceof ExplicitInitialization
  }
  // ...
}

数据库类型

数据库类型在数据库架构中定义。这意味着它们取决于您正在查询的数据库,并根据您正在分析的数据而有所不同。

例如,如果要查询 Java 项目的 CodeQL 数据库,则数据库类型可能包括 @ifstmt ,表示 Java 代码中的 if 语句,以及 @variable 表示变量。

类型兼容性

并非所有类型都兼容。例如, 4 < "five" 没有意义,因为您无法将  int 变为 string .

为了确定类型何时兼容,QL 中有许多不同的“类型域”。

  • 每个基元类型一个(除了 int 和 float ,它们位于同一个“数字”域中)。
  • 每种数据库类型一个。
  • 代数数据类型的每个分支一个。

例如,在定义类时,这会导致以下限制:

  • 一个类不能扩展多个基元类型。
  • 一个类不能扩展多个不同的数据库类型。
  • 一个类不能扩展代数数据类型的多个不同分支。