类型¶
QL 是一种静态类型语言,因此每个变量都必须声明类型。
类型是一组值。例如,类型 int
是整数集。注意,一个值可以属于多个集合,这意味着它可以具有多种类型。
QL 中的类型种类有 基本类型、类、字符类型、类域类型、代数数据类型、类型联合 和 数据库类型。
基本类型¶
这些类型是内置于 QL 中的,始终在全局 命名空间 中可用,与您正在查询的数据库无关。
- boolean: 此类型包含值
true
和false
。 - float: 此类型包含 64 位浮点数,例如
6.28
和-0.618
。 - int: 此类型包含 32 位 二进制补码 整数,例如
-1
和42
。 - string: 此类型包含有限的 16 位字符字符串。
- date: 此类型包含日期(以及可选时间)。
QL 对基本类型定义了一系列内置操作。这些操作可以通过在相应类型的表达式上使用调度来访问。例如,1.toString()
是整数常量 1
的字符串表示形式。有关 QL 中可用的所有内置操作的完整列表,请参见 QL 语言规范中的 内置函数 部分。
类¶
您可以在 QL 中定义自己的类型。其中一种方法是定义一个类。
QL 中的类不会“创建”新的对象,它只是代表一个逻辑属性。如果一个值满足该逻辑属性,则它属于特定类。
定义类¶
要定义类,您需要编写
例如
class OneTwoThree extends int {
OneTwoThree() { // characteristic predicate
this = 1 or this = 2 or this = 3
}
string getAString() { // member predicate
result = "One, two or three: " + this.toString()
}
predicate isEven() { // member predicate
this = 2
}
}
这定义了一个类 OneTwoThree
,其中包含值 1
、2
和 3
。特征谓词 捕获了“是整数 1、2 或 3 之一”的逻辑属性。
OneTwoThree
扩展了 int
,也就是说,它是 int
的子类型。QL 中的类必须始终至少有一个超类型。使用 extends 关键字引用的超类型称为类的基类型。类的值包含在超类型的交集中(也就是说,它们在 类域类型 中)。类继承其基类型的所有成员谓词。
类可以扩展多个类型。有关更多信息,请参见“多重继承”。类可以扩展最终类型(或类型的最终别名),请参见“最终扩展”。类还可以通过 instanceof 特化其他类型,而无需扩展类接口,请参见“非扩展子类型”。
- 要使类有效,它必须
- 不能扩展自身。
- 不能(传递地)扩展非最终类型和该类型的最终别名。
- 不能扩展不兼容的类型。有关更多信息,请参见“类型兼容性”。
您还可以注释类。请参阅可用于类的 注释 列表。
类主体¶
定义类时,该类还继承其超类型的所有非 私有 成员谓词和字段。
根据它们是否为最终,您可以 覆盖 或 隐藏 这些谓词和字段,以赋予它们更具体的定义。
成员谓词¶
这些是仅适用于特定类成员的 谓词。您可以在值上 调用 成员谓词。例如,您可以使用来自 上面 类的成员谓词
1.(OneTwoThree).getAString()
此调用返回结果 "One, two or three: 1"
。
表达式 (OneTwoThree)
是一个 强制转换。它确保 1
的类型为 OneTwoThree
,而不仅仅是 int
。因此,它可以访问成员谓词 getAString()
。
成员谓词特别有用,因为您可以将它们链接在一起。例如,您可以使用 toUpperCase()
,这是一个为 string
定义的内置函数
1.(OneTwoThree).getAString().toUpperCase()
此调用返回 "ONE, TWO OR THREE: 1"
。
字段¶
这些是在类的主体中声明的变量。类可以在其主体中包含任意数量的字段声明(即变量声明)。您可以在类中的谓词声明中使用这些变量。与 变量 this
类似,字段必须在 特征谓词 中受到限制。
例如
class SmallInt extends int {
SmallInt() { this = [1 .. 10] }
}
class DivisibleInt extends SmallInt {
SmallInt divisor; // declaration of the field `divisor`
DivisibleInt() { this % divisor = 0 }
SmallInt getADivisor() { result = divisor }
}
from DivisibleInt i
select i, i.getADivisor()
在此示例中,声明 SmallInt divisor
引入了字段 divisor
,在特征谓词中限制它,然后在成员谓词 getADivisor
的声明中使用它。这类似于通过在 from
部分中声明它们来在 select 子句 中引入变量。
您还可以注释谓词和字段。请参阅可用的 注释 列表。
抽象类¶
用 abstract
注释 的类,称为抽象类,也是较大类型中值的限制。但是,抽象类被定义为其子类的并集。特别地,对于一个值要属于抽象类,它必须满足类本身的特征谓词以及子类的特征谓词。请注意,最终扩展在此上下文中不被视为子类。
抽象类在您希望将多个现有类分组到一个通用名称下时非常有用。然后,您可以在所有这些类上定义成员谓词。您也可以扩展预定义的抽象类:例如,如果您导入包含抽象类的库,则可以向其中添加更多子类。
示例
如果您正在编写安全查询,您可能希望识别所有可以解释为 SQL 查询的表达式。您可以使用以下抽象类来描述这些表达式
abstract class SqlExpr extends Expr {
...
}
现在定义各种子类——每个数据库管理系统对应一个。例如,您可以定义一个子类 class PostgresSqlExpr extends SqlExpr
,其中包含传递给执行数据库查询的某个 Postgres API 的表达式。您可以为 MySQL 和其他数据库管理系统定义类似的子类。
抽象类 SqlExpr
指代所有这些不同的表达式。如果您希望稍后添加对另一个数据库系统的支持,您只需向 SqlExpr
添加一个新的子类即可;无需更新依赖于它的查询。
重要
在向现有抽象类添加新的子类时,您必须注意。添加子类不是一个孤立的更改,它还会扩展抽象类,因为抽象类是其子类的并集。
重写成员谓词¶
如果一个类从非最终的超类型继承了成员谓词,您可以重写继承的定义。您可以通过定义具有与继承谓词相同名称和元数的成员谓词,并添加 override
annotation 来执行此操作。如果您希望细化谓词以对子类中的值给出更具体的結果,这很有用。
例如,扩展来自 第一个示例 的类
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()
查询使用谓词 getAString()
的“最具体”定义,因此结果如下所示
o | getAString() result |
---|---|
1 | One or two: 1 |
2 | One or two: 2 |
3 | One, two or three: 3 |
在 QL 中,与其他面向对象语言不同,同一类型的不同子类型不需要是不相交的。例如,您可以定义 OneTwoThree
的另一个子类,它与 OneTwo
重叠
class TwoThree extends OneTwoThree {
TwoThree() {
this = 2 or this = 3
}
override string getAString() {
result = "Two or three: " + this.toString()
}
}
现在值 2 包含在两个类类型 OneTwo
和 TwoThree
中。这两个类都重写了 getAString()
的原始定义。有两个新的“最具体”定义,因此运行上面的查询会给出以下结果
o | getAString() result |
---|---|
1 | One or two: 1 |
2 | One or two: 2 |
2 | Two or three: 2 |
3 | Two or three: 3 |
多重继承¶
一个类可以扩展多个类型。在这种情况下,它从所有这些类型继承。
例如,使用上面部分的定义
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 OneTwoTree o
select o, o.getAString()
o | getAString() result |
---|---|
1 | One, two or three: 1 |
2 | One, two or three: 2 |
3 | One, two or three: 3 |
但是,当在 OneTwoFinalExtension
上调用 getAString()
时,原始定义将被遮蔽
from OneTwoFinalExtension o
select o, o.getAString()
o | getAString() result |
---|---|
1 | One or two: 1 |
2 | One or two: 2 |
非扩展子类型¶
除了扩展基本类型外,类还可以声明与其他类型的 instanceof
关系。将类声明为 instanceof Foo
粗略等同于在特征谓词中说 this instanceof Foo
。主要区别在于您可以通过 super
在 Bar
上调用方法,并且您可以获得更好的优化。
class Foo extends int {
Foo() { this in [1 .. 10] }
string fooMethod() { result = "foo" }
}
class Bar instanceof Foo {
string toString() { result = super.fooMethod() }
}
在此示例中,来自 Foo
的特征谓词也适用于 Bar
。但是,fooMethod
在 Bar
中没有公开,因此查询 select any(Bar b).fooMethod()
会导致编译时错误。从示例中注意,使用 super
关键字仍然可以在专用类中访问来自 instanceof 超类型的 方法。
至关重要的是,instanceof **超类型** 不是 **基本类型**。这意味着这些超类型不参与重写,并且此类超类型的任何字段都不是新类的一部分。这对涉及复杂类层次结构的方法解析有影响。以下示例演示了这一点。
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()
会产生 foo
。如果 Bar
被定义为 extends Foo
,那么 select any(Foo 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 注释代数数据类型,并将类保留为公共类。
例如,以下来自 CodeQL C# 数据流库的代码片段定义了用于处理受污染或未受污染值的类。在本例中,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
}
// ...
}
从 CodeQL CLI 的 2.2.0 版本开始支持类型联合。
数据库类型¶
数据库类型在数据库模式中定义。这意味着它们依赖于您要查询的数据库,并且根据您分析的数据而异。
例如,如果您要查询 Java 项目的 CodeQL 数据库,则数据库类型可能包括 @ifstmt
(代表 Java 代码中的 if 语句)和 @variable
(代表变量)。