在前面的文章中,我们首先为读者展示了如何通过LGTM查询控制台来编写和运行分析JavaScript和TypeScript代码的查询。然后,讲解了CodeQL的JavaScript标准库中用于从文本级别和词法级别分析源代码的常用类及其谓词。在本文中,我们继续讲解CodeQL的JavaScript标准库中的其他类和谓词。
用于分析JavaScript代码的CodeQL标准库
为了帮助人们分析JavaScript代码,CodeQL平台专门提供了一个功能丰富的标准库,来帮助我们分析从JavaScript项目中提取的CodeQL数据库。这个库中的类,不仅能够以面向对象的形式表示数据库中的数据,同时,该库还提供了许多抽象类和谓词,来帮助我们完成各种常见的任务。
前面的文章介绍了用于从文本级别和词法级别分析JavaScript源代码的常用类及其谓词。接下来,我们开始讲解用于从句法层次分析JavaScript源代码的类和谓词。
语法级别
Javascript 库中的大多数类都是用于将 JavaScript 程序表示为抽象语法树(abstract syntax trees,AST)的。其中,类ASTNode不仅提供了表示抽象语法树节点的所有实体,还提供了用于遍历树节点的通用谓词:
· 谓词ASTNode.getChild(i):返回本AST节点的第i个子节点。
· 谓词ASTNode.getAChild():返回本AST节点的所有子节点。
· 谓词ASTNode.getParent():返回本AST节点的父节点(如果有的话)。
需要注意的是,这些谓词只适用于执行通用的AST遍历。若要访问特定AST节点类型的子节点,应该改用在下文中介绍的专用谓词。特别是,查询不应依赖于子节点相对于其父节点的数字索引:因为在这个库的不同版本之间,这些实现细节会随之变化。
顶层代码块
从语法的角度来看,每个JavaScript程序都由一个或多个顶层代码块(或简称顶层)组成。通常情况下,一个代码块属于另一个更大的代码块,而顶层代码块则其他JavaScript代码块。 顶层代码块可以通过类TopLevel及其子类进行表示,这些类的层级结构如下所示:
· 类Toplevel
· 类 Script:一个独立的文件或 HTML < script >元素
- 类ExternalScript:一个独立的 JavaScript 文件
- 类InlineScript:嵌入在 HTML < script >标记中的代码
· 类CodeInAttribute:源自 HTML 属性值的代码块
- 类EventHandlerCode:源自事件处理程序属性(如 onload)的代码
- 类JavaScriptURL:从带有javascript:的URL 中提取的代码
· 类Externs:包含 Externs(详情请参考https://developers.google.com/closure/compiler/docs/api-tutorial3#externs) 定义的 JavaScript 文件
TopLevel类还提供以下成员谓词:
· 谓词TopLevel.getNumberOfLines(),返回顶层代码块中代码的总行数(包括代码行、注释行和空白行)。
· 谓词TopLevel.getNumberOfLinesOfCode() ,返回代码行数,即至少包含一个单词的行数。
· 谓词TopLevel.getNumberOfLinesOfComments(),返回包含或属于注释的行数。
· 谓词TopLevel.isMinified(),根据每行的平均语句数,使用启发式方法确定顶层代码块中是否包含压缩型代码(minified code)。这里所谓的压缩,是指从源代码中删除不必要的字符,使它看起来简单而整洁。
需要注意的是,在默认的情况下,LGTM会过滤掉压缩型顶层代码块中的警报,因为它们通常难以解释。当我们在LGTM查询控制台编写自定义的查询时,这种过滤不是自动执行的,因此,如果希望执行该操作的话,可以显式一个类似and not e.getTopLevel().isMinified()的查询条件,以过滤来自压缩型代码的返回结果。
语句与表达式
除了子类TopLevel之外,类ASTNode最重要的子类就是Stmt和Expr了,通过与其他子类结合使用,这两个子类就能用于表示语句和表达式。接下来,我们为读者介绍用于表示语句与表达式的类和谓词。有关Stmt和Expr的所有子类及其API的完整资料,请参见Stmt.qll(地址为https://help.semmle.com/qldoc/javascript/semmle/javascript/Stmt.qll/module.Stmt.html)和 Expr.qll(地址为https://help.semmle.com/qldoc/javascript/semmle/javascript/Expr.qll/module.Expr.html)。
· 类Stmt:可以使用谓词ControlStmt.getAControlledStmt()来访问包含该语句的最内层函数或顶层代码块。
· 类ControlStmt:用于控制其他语句(即条件语句、循环语句、try语句或 with 语句)执行的语句;可以使用谓词ControlStmt.getAControlledStmt()来访问它控制的语句。
- 类IfStmt:用于表示一个if语句;可以使用谓词IfStmt.getCondition()、IfStmt.getThen()和IfStmt.getElse()来访问其条件表达式的“then”分支和“else”分支。
- 类LoopStmt:表示循环语句;可以使用谓词Loop.getBody()和Loop.getTest()来访问该语句的循环主体和测试表达式。
· 类WhileStmt、 DoWhileStmt:分别表示“while”循环语句和“do-while”循环语句。
· 类ForStmt:表示“for”语句;可以通过谓词ForStmt.getInit()和ForStmt.getUpdate()来访问初始条件和更新条件。
· 类EnhancedForLoop:表示“for-in”或“for-of”语句;可以通过谓词EnhancedForLoop.getIterator()来访问循环迭代器(可能是表达式或变量声明) ,并通过谓词EnhancedForLoop.getIterationDomain()来访问正在迭代的表达式。
· 类ForInStmt、 ForOfStmt:分别用于表示“for-in”和“for-of”循环语句。
· 类WithStmt:表示“with”语句;可以通过谓词WithStmt.getExpr()和WithStmt.getBody()分别访问控制表达式和 with语句的主体部分。
· 类SwitchStmt:表示“switch”语句;可以使用成员谓词SwitchStmt.getExpr()来访问“switch”语句的表达式部分;可以使用成员谓词SwitchStmt.getCase(int)和SwitchStmt.getACase()来访问单个case分支;每个case分支都可以通过类 Case的实例来表示,其成员谓词包括 Case.getExpr()和Case.getBodyStmt(int),前者可用于读取case分支中的表达式,后者用于读取case分支的主体部分。
· 类TryStmt:表示“try”语句;可以通过成员谓词TryStmt.getBody()、TryStmt.getCatchClause() 和TryStmt.getFinally来访问该语句的主体部分、“catch”子句和“finally”语句块。
· 类BlockStmt:表示一个语句块;可以使用成员谓词BlockStmt.getStmt(int)来访问该语句块中的单个语句。
· 类ExprStmt:表示一个表达式语句;可以使用 成员谓词ExprStmt.getExpr()来访问表达式本身。
· 类JumpStmt:表示会打乱结构化控制流的语句,即 break语句、continue语句、return语句和throw语句;可以使用谓词JumpStmt.getTarget() 来确定跳转目标,该目标可以是一个语句或(用于return和未被捕获的throw语句)外层函数。
· 类BreakStmt:表示“break”语句;可以使用成员谓词BreakStmt.getLabel()来访问其(可选)目标标签。
· 类ContinueStmt:表示“continue”语句;可以使用成员谓词ReturnStmt.getExpr()来访问其(可选)目标标签。
· 类ThrowStmt:表示“return”语句;可以使用成员谓词ReturnStmt.getExpr() 来访问其(可选)结果表达式。
· 类ReturnStmt:表示“throw”语句;可以使用ThrowStmt.getExpr()来访问其表达式。
· 类FunctionDeclStmt:表示函数声明语句;其成员谓词,请参见下文。
· 类ClassDeclStmt:表示类声明语句;其成员谓词,请参见下文。
· 类DeclStmt:表示包含一个或多个声明符的声明语句,这些声明符可通过成员谓词 DeclStmt.getDeclarator(int)进行访问。
· 类VarDeclStmt、ConstDeclStmt、LetStmt:分别表示声明var、const 和let 的语句。
· 类Expr:使用成员谓词Expr.getEnclosingStmt()可以获取该表达式所属的最内层语句;通过成员谓词可以确定该表达式是否没有副作用。
· 类Identifier:用于表示标识符;可以使用成员谓词Identifier.getName() 来获取其名称。
· 类Literal:表示字面值;可以使用成员谓词Literal.getValue()来获取其值的字符串表示形式,通过成员谓词Literal.getRawValue()则可以获取其原始源代码文本(包括字符串文本前后的引号)。
· 类NullLiteral、BooleanLiteral、NumberLiteral、StringLiteral、RegExpLiteral:表示不同类型的字面值。
· 类ThisExpr:表示“this”表达式。
· 类SuperExpr:表示“super”表达式。
· 类ArrayExpr:表示数组表达式;可以使用成员谓词ArrayExpr.getElement(i)来获得第i个元素表达式,使用成员谓词ArrayExpr.elementIsOmitted(i)则可以检查第i个元素是否被省略。
· 类ObjectExpr:表示对象表达式;使用成员谓词ObjectExpr.getProperty(i)可以获取对象表达式中的第i个属性;属性可以通过类Property表示,下文中将对其进行更详细的描述。
· 类FunctionExpr:表示一个函数表达式;其成员谓词将在下文中加以解释。
· 类ArrowFunctionExpr:表示ECMAScript2015风格的箭头函数表达式,其成员谓词将在下文中加以介绍。
· 类ClassExpr:表示类表达式,其成员谓词将在下文中加以介绍。
· 类ParExpr:表示一个带括号的表达式;使用成员谓词ParExpr.getExpression()可以获取操作数表达式;对于任何表达式而言,都可以通过成员谓词Expr.stripParens()递归地去掉所有的括号。
· 类SeqExpr:表示由逗号运算符连接的两个或多个表达式所组成的序列;我们可以使用SeqExpr.getOperand(i)获得第i个子表达式。
· 类ConditionalExpr:表示三值条件表达式;成员谓词ConditionalExpr.getCondition()、ConditionalExpr.getConsequent()和ConditionalExpr.getAlternate()分别用于访问条件表达式、“then”表达式和“else”表达式。
· 类InvokeExpr:表示函数调用或“new”表达式;成员谓词InvokeExpr.getCallee()用来访问用于指定要调用的函数的表达式,成员谓词InvokeExpr.getArgument(i)用于访问第i个参数表达式。
· 类CallExpr:表示函数调用表达式。
· 类NewExpr:表示“new”表达式。
· 类MethodCallExpr:表示一个函数调用表达式,其被调用方表达式为属性访问表达式;可以使用MethodCallExpr.getReceiver来访问方法调用接收方的表达式,并使用MethodCallExpr.getMethodName()来获取方法名称(如果可以静态确定的话)。
· 类PropAccess:一种属性访问表达式,即形式为e.f的“点号”表达式或形式为e[p]的索引表达式;使用PropAccess.getBase()可以获取表示要访问谁的属性的基本表达式(在本例中为e),并使用PropAccess.getPropertyName()确定访问的属性的名称;如果不能静态确定名称的话,那么调用getPropertyName()时不会返回任何值。
· 类DotExpr:表示“点号”表达式。
· 类IndexExpr:表示索引表达式(也称为计算型属性访问表达式)。
· 类UnaryExpr:表示一元表达式;可以使用UnaryExpr.getOperand()来获得操作数表达式。
· 类NegExpr(“-”)、PlusExpr(“+”)、LogNotExpr(“!”)、BitNotExpr(“?”)、TypeofExpr、VoidExpr、DeleteExpr、SpreadElement(“…”):表示各种类型的一元表达式。
· 类BinaryExpr:表示二元表达式;可以使用BinaryExpr.getLeftOperand()和BinaryExpr.getRightOperand()访问操作数表达式。
· 类Comparison:表示各种类型的比较表达式。
· 类EqualityTest:表示各种相等性或不相等性测试表达式。
· 类EqExpr(“==”)、NEqExpr(“!=”):非严格型相等性和不相等性测试表达式。
· 类StrictEqExpr(“===”)、StrictNEqExpr(“!==”):严格的相等性和不相等性测试表达式。
· 类LTExpr(“< ”)、LEExpr(“< =”)、GTExpr(“ >”)、GEExpr(“ >=”):数字比较表达式。
· 类LShiftExpr(“<< ”)、RShiftExpr(“ >>”)、URShiftExpr(“ >>>”):移位运算符表达式。
· 类AddExpr(“+”)、SubExpr(“-”)、MulExpr(“*”)、DivExpr(“/”)、ModExpr(“%”)、ExpExpr(“**”):算术运算符表达式。
· 类BitOrExpr(“|”)、XOrExpr(“^”)、BitAndExpr(“&”):按位运算符表达式。
· 类InExpr:基于in的类型测试表达式。
· 类InstanceofExpr:基于instanceof的类型测试表达式。
· 类LogAndExpr(“&&”),LogOrExpr(“||”):短路型逻辑运算表达式。
· 类Assignment:表示赋值表达式,可以是简单的赋值表达式,也可以是复合的赋值表达式;可以使用Assignment.getLhs()和Assignment.getRhs()分别访问表达式的左侧和右侧部分。
· 类AssignExpr:表示简单的赋值表达式。
· 类CompoundAssignExpr:表示复合赋值表达式。
· 类AssignAddExpr、AssignSubExpr、AssignMulExpr、AssignDivExpr、AssignModExpr、AssignLShiftExpr、AssignRShiftExpr、AssignURShiftExpr、AssignOrExpr、AssignXOrExpr、AssignAndExpr、AssignExpExpr:表示各种类型的复合赋值表达式。
· 类UpdateExpr:表示递增或递减表达式;使用UpdateExpr.getOperand()可以获得操作数表达式。
· 类PreIncExpr,PostIncExpr:表示递增表达式。
· 类PreDecExpr,PostDecExpr:表示递减表达式。
· 类YieldExpr:表示“yield”表达式;通过YieldExpr.getOperand()可以访问(可选的)操作数表达式;通过YieldExpr.isDelegating()可以检查是否为委派型yield *表达式。
· 类TemplateLiteral:表示ECMAScript 2015模板文字;可以通过TemplateLiteral.getElement(i)返回模板的第i个元素,该元素可以是插值表达式或常量模板元素。
· 类TaggedTemplateExpr:ECMAScript 2015带标签的模板文字;使用TaggedTemplateExpr.getTag()可以访问标记表达式,而使用TaggedTemplateExpr.getTemplate()则可以访问被标记的模板文字。
· 类TemplateElement:表示常量模板元素;对于文字,可以使用TemplateElement.getValue()获取元素的值;此外,也可以使用TemplateElement.getRawValue()访问其原始值
· 类AwaitExpr:表示“await”表达式;可以使用AwaitExpr.getOperand()访问操作数表达式。
· 类Stmt和Expr共享一个公共超类ExprOrStmt,对于涉及语句或表达式,但是不牵扯任何其他AST节点的查询来说,它是非常有用的。
下面的查询是用于演示如何使用表达式AST节点的,具体来说就是查找形为e+f>>g的表达式;实际上,为了阐明运算符的优先级,这些表达式应改写为(e+f)>>g:
import javascript from ShiftExpr shift, AddExpr add where add = shift.getAnOperand() select add, "This expression should be bracketed to clarify precedence rules."
下图展示了该查询的运行结果:
函数
JavaScript提供了多种定义函数的方式:在ECMAScript 5中,提供了多种函数声明语句和函数表达式声明语句;在ECMAScript 2015中,又增加了箭头函数表达式。对于这些语法形式来说,我们可以分别通过类FunctionDeclStmt(类Stmt的一个子类)、FunctionExpr(类Expr的一个子类)和ArrowFunctionExpr(也是类Expr的一个子类)来进行表示。实际上,这三个类都是类Function的子类,因此,都提供了访问函数参数或函数体所需的成员谓词:
成员谓词Function.getId()可以返回命名函数的标识符。
成员谓词Function.getParameter(i)和Function.getAParameter()分别用于访问第i个参数或任意参数;而参数通常是由类Parameter来表示的,它是类BindingPattern的子类(详情见下文)。
成员谓词Function.getBody()用于返回函数体,它通常是一个Stmt,但也可能是表示箭头函数表达式和遗留表达式闭包的Expr。
下面是一个查找所有表达式闭包的查询示例:
import javascript from FunctionExpr fe where fe.getBody() instanceof Expr select fe, "Use arrow expressions instead of expression closures."
下面是上述代码的返回结果:
如您所见,在这里的演示项目中并没有找到符合要求的表达式闭包。
接下来,我们看看另一个查询示例,其作用是查找具有两个绑定了同一个变量的参数的函数:
import javascript from Function fun, Parameter p, Parameter q, int i, int j where p = fun.getParameter(i) and q = fun.getParameter(j) and i < j and p.getAVariable() = q.getAVariable() select fun, "This function has two parameters that bind the same variable."
上述代码的运行结果如下所示:
如您所见,在演示项目中并没有找到符合要求的函数。
类
类既可以通过CodeQL类ClassDeclStmt(它是类Stmt的子类)表示的类声明语句进行定义,也可以通过CodeQL类ClassExpr(它是类Expr的子类)表示的类表达式进行定义。实际上,这两个类都是ClassDefinition的子类,并继承了用于访问类的名称、其超类和类主体的成员谓词:
· 成员谓词ClassDefinition.getIdentifier()用于返回命名函数的标识符。
· 成员谓词ClassDefinition.getSuperClass()用于返回指定超类的Expr,该超类可能尚未定义。
· 成员谓词ClassDefinition.getMember(n)用于返回这个类的第n个成员的定义。
· 成员谓词ClassDefinition.getMethod(n)用于将ClassDefinition.getMember(n)限定为返回成员方法(而不是字段)。
· 成员谓词ClassDefinition.getField(n)用于将ClassDefinition.getMember(n)限定为返回字段(而不是方法)。
· 成员谓词ClassDefinition.getConstructor() 用于获取类的构造函数。
注意,类的字段还不是一个标准的语言特性,因此,它们表示的细节可能会发生变化。
方法的定义通常是由类MethodDefinition来表示的,它(与其对应的FieldDefinition类似)是MemberDefinition的子类。该类提供了以下重要成员谓词:
· MemberDefinition.isStatic():对于静态成员,则该判断成立。
· MemberDefinition.isComputed():如果成员的名称是在运行时确定的,则该判断成立。
· MemberDefinition.getName():获取成员的名称(如果可以静态确定的话)。
· MemberDefinition.getInit():获取字段的初始化代码:对于方法来说,初始化代码是一个函数表达式;对于字段来说,其初始化代码可能是一个任意表达式,也可能是未定义的。
此外,这里还有三个用于表示特殊方法的类:类ConstructorDefinition表示构造函数,而类GetterMethodDefinition和SetterMethodDefinition分别表示getter和setter方法。
小结
前面的文章介绍了用于从文本级别和词法级别分析JavaScript源代码的常用类及其谓词,在本文中,我们为读者讲解了用于从句法层次分析JavaScript源代码的类和谓词。
备注:本系列文章乃本人在学习CodeQL平台过程中所做的笔记,希望能够对大家有点滴帮助——若果真如此的话,本人将备感荣幸。
参考资料:https://help.semmle.com/
如若转载,请注明原文地址