TypeScript 的 CodeQL 库¶
当您分析 TypeScript 程序时,您可以利用 TypeScript 的 CodeQL 库中大量类。
概述¶
对分析 TypeScript 代码的支持与 JavaScript 的 CodeQL 库捆绑在一起,因此您可以通过导入 javascript.qll 模块来包含完整的 TypeScript 库。
import javascript
JavaScript 的 CodeQL 库涵盖了此库的大部分内容,并且与 TypeScript 分析也相关。本文档以 TypeScript 特定的类和谓词补充了 JavaScript 文档。
语法¶
TypeScript 中的大多数语法以与 JavaScript 对应语法相同的方式表示。例如,a+b 由 AddExpr 表示;与 JavaScript 中一样。另一方面,x as number 由 TypeAssertion 表示,该类是 TypeScript 特定的类。
类型注释¶
TypeExpr 类表示类型注释的一部分。
只有源代码中明确的类型注释才会作为 TypeExpr 出现。TypeScript 编译器推断的类型是 Type 实体;有关此方面的详细信息,请参阅关于 静态类型信息 的部分。
有几种方法可以访问类型注释,例如
VariableDeclaration.getTypeAnnotation()Function.getReturnTypeAnnotation()BindingPattern.getTypeAnnotation()Parameter.getTypeAnnotation()(BindingPattern.getTypeAnnotation()的特例)VarDecl.getTypeAnnotation()(BindingPattern.getTypeAnnotation()的特例)FieldDeclaration.getTypeAnnotation()
TypeExpr 类提供了一些方便的成员谓词,如 isString() 和 isVoid() 来识别常用的类型。
表示类型注释的子类为
- TypeAccess:引用类型的名称,如
Date或http.ServerRequest。- LocalTypeAccess:未限定的名称,如
Date。 - QualifiedTypeAccess:以命名空间为前缀的名称,如
http.ServerRequest。 - ImportTypeAccess:用作类型的
import,如import("./foo")。
- LocalTypeAccess:未限定的名称,如
- PredefinedTypeExpr:预定义类型,如
number、string、void或any。 - ThisTypeExpr:
this类型。 - InterfaceTypeExpr,也称为文字类型,如
{x: number}。 - FunctionTypeExpr:
(x: number) => string之类的类型。 - GenericTypeExpr:具有类型参数的命名类型,如
Array<string>。 - LiteralTypeExpr:用作类型的字符串、数字或布尔常量,如
'foo'。 - ArrayTypeExpr:
string[]之类的类型。 - UnionTypeExpr:
string | number之类的类型。 - IntersectionTypeExpr:
S & T之类的类型。 - IndexedAccessTypeExpr:
T[K]之类的类型。 - ParenthesizedTypeExpr:
(string)之类的类型。 - TupleTypeExpr:
[string, number]之类的类型。 - KeyofTypeExpr:
keyof T之类的类型。 - TypeofTypeExpr:
typeof x之类的类型。 - IsTypeExpr:
x is string之类的类型。 - MappedTypeExpr:
{ [K in C]: T }之类的类型。
还有一些子类可能是类型注释的一部分,但它们本身不是类型。
- TypeParameter:在类型或函数上声明的类型参数,如
class C<T> {}中的T。 - NamespaceAccess:从类型内部引用命名空间的名称,如
http.ServerRequest中的http。- LocalNamespaceAccess:前缀中的初始标识符,如
http.ServerRequest中的http。 - QualifiedNamespaceAccess:前缀中的限定名称,如
net.client.Connection中的net.client。 - ImportNamespaceAccess:用作类型中命名空间的
import,如import("http").ServerRequest中那样。
- LocalNamespaceAccess:前缀中的初始标识符,如
- VarTypeAccess:对类型内部值的引用,如
typeof x或x is string中的x。
函数签名¶
Function 类是一个广泛的类,它既包括具体的函数,也包括函数签名。
函数签名可以采取多种形式
- 函数类型,如
(x: number) => string。 - 抽象方法,如
abstract foo(): void。 - 重载签名,如
foo(x: number): number后跟foo的实现。 - 调用签名,如
{ (x: string): number }中那样。 - 索引签名,如
{ [x: string]: number }中那样。 - 处于环境上下文中的函数,如
declare function foo(x: number): string。
建议您使用谓词 Function.hasBody() 来区分具体的函数和签名。
类型参数¶
TypeParameter 类表示类型参数,而 TypeParameterized 类表示可以声明类型参数的实体。类、接口、类型别名、函数和映射类型表达式都是 TypeParameterized。
您可以使用以下谓词访问类型参数
TypeParameterized.getTypeParameter(n)获取第n个声明的类型参数。TypeParameter.getHost()获取声明给定类型参数的实体。
您可以使用以下谓词访问类型参数
GenericTypeExpr.getTypeArgument(n)获取类型的第n个类型参数。TypeAccess.getTypeArgument(n)是上述内容的便捷替代方法(具有类型参数的 TypeAccess 包含在 GenericTypeExpr 中)。InvokeExpr.getTypeArgument(n)获取调用的第n个类型参数。ExpressionWithTypeArguments.getTypeArgument(n)获取泛型超类表达式的第n个类型参数。
要选择对给定类型参数的引用,请使用 getLocalTypeName()(请参阅下面的 名称绑定)。
示例¶
选择将值强制转换为类型参数的表达式
import javascript
from TypeParameter param, TypeAssertion assertion
where assertion.getTypeAnnotation() = param.getLocalTypeName().getAnAccess()
select assertion, "Cast to type parameter."
类和接口¶
CodeQL 类 ClassOrInterface 是类和接口的通用超类型,并提供一些 TypeScript 特定的成员谓词
ClassOrInterface.isAbstract()在此为接口或具有abstract修饰符的类时成立。ClassOrInterface.getASuperInterface()从类的implements子句或接口的extends子句获取类型。ClassOrInterface.getACallSignature()获取接口的调用签名,例如在{ (arg: string): number }中。ClassOrInterface.getAnIndexSignature()获取索引签名,例如在{ [key: string]: number }中。ClassOrInterface.getATypeParameter()获取声明的类型参数(TypeParameterized.getATypeParameter()的特殊情况)。
请注意,类的超类是一个表达式,而不是类型注释。如果超类具有类型参数,它将是 ExpressionWithTypeArguments 类型的表达式。
另请参阅“CodeQL 库 for JavaScript”中的类文档。
要选择对类或接口的类型引用,请使用 getTypeName()。
语句¶
以下是 TypeScript 特定的语句
- NamespaceDeclaration:如
namespace M {}的语句。 - EnumDeclaration:如
enum Color { red, green, blue }的语句。 - TypeAliasDeclaration:如
type A = number的语句。 - InterfaceDeclaration:如
interface Point { x: number; y: number; }的语句。 - ImportEqualsDeclaration:如
import fs = require("fs")的语句。 - ExportAssignDeclaration:如
export = M的语句。 - ExportAsNamespaceDeclaration:如
export as namespace M的语句。 - ExternalModuleDeclaration:如
module "foo" {}的语句。 - GlobalAugmentationDeclaration:如
global {}的语句
表达式¶
以下是 TypeScript 特定的表达式
- ExpressionWithTypeArguments:当类的
extends子句具有类型参数时出现,例如在class C extends D<string>中。 - TypeAssertion:断言值为给定类型,例如
x as number或<number> x。 - NonNullAssertion:断言值不为空或未定义,例如
x!。 - ExternalModuleReference:导入分配右侧的
require调用,例如import fs = require("fs")。
环境声明¶
类型注释、接口和类型别名被认为是环境 AST 节点,任何具有 declare 修饰符的节点也是如此。
谓词 ASTNode.isAmbient() 可用于确定 AST 节点是否为环境节点。
环境节点主要被控制流和数据流分析忽略。环境声明的最外层在控制流图中具有单个无操作节点,并且没有内部控制流。
静态类型信息¶
静态类型信息和全局名称绑定可用于启用了“完整”TypeScript 提取的项目。此选项在使用 CodeQL CLI 创建数据库时默认启用。
基本用法¶
Type 类表示静态类型,例如 number 或 string。可以使用 Expr.getType() 获取表达式的类型。
可以通过多种方式识别引用特定命名类型的类型
type.(TypeReference).hasQualifiedName(name)在类型引用给定命名类型时成立。type.(TypeReference).hasUnderlyingType(name)在类型引用给定命名类型或其传递子类型时成立。type.hasUnderlyingType(name)与上面类似,但还适用于引用包含在联合类型和/或交叉类型中时。
hasQualifiedName 和 hasUnderlyingType 谓词有两种重载
- 单参数版本采用相对于全局范围的限定名称。
- 双参数版本采用模块的名称和相对于该模块的限定名称。
示例¶
以下查询可用于查找对 Node.js Buffer 对象的所有 toString 调用
import javascript
from MethodCallExpr call
where call.getReceiver().getType().hasUnderlyingType("Buffer")
and call.getMethodName() = "toString"
select call
使用类型¶
Type 实体不与特定源位置相关联。例如,number 关键字可能有多种用法,但只有一个 number 类型。
Type 的一些重要成员谓词是
Type.getProperty(name)获取命名属性的类型。Type.getMethod(name)获取命名方法的签名。Type.getSignature(kind,n)获取调用或构造函数签名的第n个重载。Type.getStringIndexType()获取字符串索引签名的类型。Type.getNumberIndexType()获取数字索引签名的类型。
Type 实体始终属于以下子类之一
TypeReference:命名类型,可能具有类型参数。UnionType:联合类型,例如string | number。IntersectionType:交叉类型,例如T & U。TupleType: 元组类型,例如[string, number]。StringType:string类型。NumberType:number类型。AnyType:any类型。NeverType:never类型。VoidType:void类型。NullType:null类型。UndefinedType:undefined类型。ObjectKeywordType:object类型。SymbolType:symbol或unique symbol类型。AnonymousInterfaceType: 匿名类型,例如{x: number}。TypeVariableType: 对类型变量的引用。ThisType: 特定类型中的this类型。TypeofType: 具名值的类型,例如typeof X。BooleanLiteralType:true或false类型。StringLiteralType: 字符串常量的类型。NumberLiteralType: 数字常量的类型。
此外,Type 还有以下子类,它们与上述子类部分重叠。
BooleanType:boolean类型,在内部表示为联合类型true | false。PromiseType: 描述承诺的类型,例如Promise<T>。ArrayType: 描述数组对象的类型,可能是元组类型。PlainArrayType: 形式为Array<T>的类型。ReadonlyArrayType: 形式为ReadonlyArray<T>的类型。
LiteralType: 布尔值、字符串或数字字面量类型。NumberLikeType:number类型或数字字面量类型。StringLikeType:string类型或字符串字面量类型。BooleanLikeType:true、false或boolean类型。
规范名称和具名类型¶
CanonicalName 是一个 CodeQL 类,它表示相对于根范围(例如模块或全局范围)的限定名称。它通常代表一个实体,例如类型、命名空间、变量或函数。 TypeName 和 Namespace 是此类的子类。
可以使用 hasQualifiedName 谓词识别规范名称。
hasQualifiedName(name)如果限定名称相对于全局范围为name则成立。hasQualifiedName(module,name)如果限定名称相对于给定模块名称为name则成立。
为方便起见,此谓词在其他类(例如 TypeReference 和 TypeofType)中也可用,它转发到底层规范名称。
函数类型¶
没有用于函数类型的 CodeQL 类,因为任何具有调用或构造签名类型的函数都可用作函数。类型 CallSignatureType 表示此类签名(带或不带 new 关键字)。
可以通过几种方式获得签名。
Type.getFunctionSignature(n)获取第n个重载函数签名。Type.getConstructorSignature(n)获取第n个重载构造函数签名。Type.getLastFunctionSignature()获取最后声明的函数签名。Type.getLastConstructorSignature()获取最后声明的构造函数签名。
CallSignatureType 的一些重要成员谓词是
CallSignatureType.getParameter(n)获取第n个参数的类型。CallSignatureType.getParameterName(n)获取第n个参数的名称。CallSignatureType.getReturnType()获取返回值类型。
请注意,签名与特定声明位置无关。
调用解析¶
对于调用表达式,有其他类型信息可用。
InvokeExpr.getResolvedCallee()获取被调用者作为具体的Function。InvokeExpr.getResolvedCalleeName()获取被调用者作为规范名称。InvokeExpr.getResolvedSignature()获取被调用函数的签名,其中重载已解析,类型参数已替换。
请注意,这些引用由类型系统确定的调用目标。实际调用目标可能在运行时不同,例如,如果目标是已在子类中覆盖的方法。
继承和子类型化¶
可以使用 TypeName.getABaseTypeName() 获取具名类型的声明超类型。
这在类型名称级别进行操作,因此继承链中使用的特定类型参数不可用。但是,这些通常可以使用 Type.getProperty 或 Type.getMethod 推断出来,它们都考虑了继承。
这仅考虑在类型的 extends 或 implements 子句中显式提及的类型。没有谓词可以确定一般情况下类型之间的子类型化或可赋值性。
以下两个谓词可用于识别给定类型的子类型。
Type.unfold()展开联合和/或交集类型并获取底层类型,如果它不是联合或交集,则获取类型本身。Type.hasUnderlyingType(name)如果类型是给定具名类型的引用,则成立,可能是在展开联合/交集并遵循声明的超类型之后。
示例¶
以下查询可用于查找所有作为 React 组件的类,以及它们的 props 属性的类型,这通常与其第一个类型参数一致。
import javascript
from ClassDefinition cls, TypeName name
where name = cls.getTypeName()
and name.getABaseTypeName+().hasQualifiedName("React.Component")
select cls, name.getType().getProperty("props")
名称绑定¶
在 TypeScript 中,名称可以引用变量、类型和命名空间,或者这些的组合。
这些概念被建模为不同的实体:Variable、TypeName 和 Namespace。例如,下面的类 C 引入了变量和类型。
class C {}
let x = C; // refers to the variable C
let y: C; // refers to the type C
变量 C 和类型 C 被建模为不同的实体。一个是 Variable,另一个是 TypeName。
TypeScript 还允许您导入类型和命名空间,并在不同的范围内为它们提供本地名称。例如,下面的导入引入了本地类型名称 B。
import {C as B} from "./foo"
本地名称 B 表示为一个名为 B 的 LocalTypeName,它仅限于包含导入的文件。导入语句还可以引入 Variable 和 LocalNamespaceName。
下表显示了用于处理每种名称的相关类。这些类将在下面详细描述。
| 种类 | 本地别名 | 规范名称 | 定义 | 访问 |
|---|---|---|---|---|
| 值 | 变量 | VarAccess | ||
| 类型 | LocalTypeName | TypeName | TypeDefinition | TypeAccess |
| 命名空间 | LocalNamespaceName | 命名空间 | NamespaceDefinition | NamespaceAccess |
注意: TypeName 和 Namespace 仅在使用完整的 TypeScript 提取生成数据库时才填充。 LocalTypeName 和 LocalNamespaceName 始终填充。
类型名称¶
一个 TypeName 是对类型的限定名称,不绑定到特定词法范围。 TypeDefinition 类表示定义类型的实体,即类、接口、类型别名、枚举或枚举成员。 用于处理类型名的相关谓词是
TypeAccess.getTypeName()获取引用的限定名称(如果有)。TypeDefinition.getTypeName()获取类、接口、类型别名、枚举或枚举成员的限定名称。TypeName.getAnAccess()获取对给定类型的访问。TypeName.getADefinition()获取给定类型的定义。 注意,接口可以有多个定义。
一个 LocalTypeName 的行为类似于块级作用域变量,即它具有非限定名称,并且仅限于特定范围。 相关谓词是
LocalTypeAccess.getLocalTypeName()获取非限定类型访问引用的本地名称。LocalTypeName.getAnAccess()获取对本地类型名的访问。LocalTypeName.getADeclaration()获取此名称的声明。LocalTypeName.getTypeName()获取此名称所指的限定名称。
示例¶
查找省略对泛型类型的类型参数的引用。
最好使用 TypeName 来解析导入和限定名称
import javascript
from TypeDefinition def, TypeAccess access
where access.getTypeName().getADefinition() = def
and def.(TypeParameterized).hasTypeParameters()
and not access.hasTypeArguments()
select access, "Type arguments are omitted"
查找用作类型和值的导入名称
import javascript
from ImportSpecifier spec
where exists (LocalTypeAccess access | access.getLocalTypeName().getADeclaration() = spec.getLocal())
and exists (VarAccess access | access.getVariable().getADeclaration() = spec.getLocal())
select spec, "Used as both variable and type"
命名空间名称¶
命名空间由 Namespace 和 LocalNamespaceName 类表示。 NamespaceDefinition 类表示命名空间的语法定义,包括普通命名空间声明和枚举声明。
注意,这些类只处理从类型注释中引用的命名空间,而不是通过表达式引用的命名空间。
一个 Namespace 是命名空间的限定名称,不绑定到特定范围。 用于处理命名空间的相关谓词是
NamespaceAccess.getNamespace()获取命名空间访问引用的命名空间。NamespaceDefinition.getNamespace()获取由命名空间或枚举声明定义的命名空间。Namespace.getAnAccess()获取对类型内部的命名空间的访问。Namespace.getADefinition()获取此命名空间的定义。 注意,命名空间可以有多个定义。Namespace.getNamespaceMember(name)获取具有给定名称的内部命名空间。Namespace.getTypeMember(name)获取以给定名称导出的类型。Namespace.getAnExportingContainer()获取一个 StmtContainer,其导出内容会贡献到此命名空间。 这可以是命名空间声明的主体或模块的顶层。 枚举没有导出容器。
一个 LocalNamespaceName 的行为类似于块级作用域变量,即它具有非限定名称,并且仅限于特定范围。 相关谓词是
LocalNamespaceAccess.getLocalNamespaceName()获取标识符引用的本地名称。LocalNamespaceName.getAnAccess()获取引用此本地名称的标识符。LocalNamespaceName.getADeclaration()获取声明此本地名称的标识符。LocalNamespaceName.getNamespace()获取此名称所指的命名空间。