编写代码检查规则的神器,解读CodeNavi语法结构
本文分享自华为云社区《CodeNavi 规则的语法结构》,作者: Uncle_Tom。 1. 代码和检查规则 1.1. 代码的构成 程序是由空格分隔的字符串组成的序列。在程序分析中,这一个个的字符串被称为"token",是源代码中的最小语法单位,是构成编程语言语法的基本元素。 Token可以分为多种类型,常见的有关键字(如if、while)、标识符(变量名、函数名)、字面量(如数字、字符串)、运算符(如+、-、*、/)、分隔符(如逗号,、分号;)等。 程序在编译过程中,词法分析器(Lexer)读取源代码并将其分解成一系列的token。语法分析器(Parser)会使用这些 token 来构建一个抽象语法树(Abstract Syntax Tree, AST),这个树结构表示了代码的语法结构。这个时候每个 token 也可以称为抽象语法树的节点,树上某个节点的分支就是这个节点的子节点。每个节点都会有节点类型、属性、值。 这些是程序员非常容易理解的。 1.2. 检查规则 检查规则是查找代码节点中符合缺陷模式的检查规则。 通常静态分析工具的规则编写采用程序语言,这样开发人员除了要了解检查的问题本省,还需要理解静态分析理论,以及静态分析工具的框架,无形中增加了开发的难度。所以我们一直期望寻找到一种更适合编写静态分析规则的语言,让开发人员能够更好的专注于理解检查问题的本身,而不需要花更多的时间在如何实现上。 《寻找适合编写静态分析规则的语言》中我们给出了两个问题的解决,来描述我们对编写静态分析规则的期望。 例如: 检查问题: 生产环境中不应该有调试代码。 问题检查条件: 查找所有函数声明 并且(And):函数名以"debug"开头 并且(And):函数只有一个参数 并且(And):参数类型为"java.util.List" 问题代码样例 package com.dsl; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.List; /** * 检查问题:生产环境中不应该有调试代码。 * 问题检查条件: * - 查找所有函数声明; * - 并且(And):函数名以"debug"开头; * - 并且(And):函数只有一个参数; * - 并且(And):参数类型为"java.util.List"。 */ public class CheckDebug { private static final Logger LOG = LogManager.getLogger(CheckDebug.class); // 应检查出的问题函数 public void debugFunction(List<String> msgs) { for (String msg : msgs) { LOG.error("print debug info: {}", msg); } } } 编写检查规则 DSL 写的检查规则 /** * 检查问题:生产环境中不应该有调试代码。 * 问题检查条件: * - 查找所有函数声明; * - 并且(And):函数名以"debug"开头; * - 并且(And):函数只有一个参数; * - 并且(And):参数类型为"java.util.List"。 */ functionDeclaration fd where and( fd.name startWith "debug", fd.parameters.size() == 1, fd.parameters[0].type.name == "java.util.List" ); 2. 规则语法结构 下面我们进一步给出这种用于编写代码检查规则的工具 CodeNavi 的语法结构。 2.1. 规则的语法 结构图 语法说明 规则语句是以分号 (;) 结尾, 作为规则的终止符。 2.2. 语句结构 结构图 语法说明 节点(node): 为待检测代码节点(token); as 别名:给要查找的节点一个别名,便于后面的使用。as可以省略。别名的命名规则如下: 只能包含大小写英文字母和数字; 数字只能出现在末尾; 不能和规则内置的节点或属性同名。 例如:va1、fd、Field1都是合法的别名,1fc、v2a是非法别名。 查询条件(condition):筛选节点的条件; 2.3. 节点和节点属性 结构图 语法说明 节点(node):是代码对应语法树的节点或子节点; 属性(attribute):是代码对应语法树的节点或子节点的属性; .: 节点、子节点、属性之间的连接符; 2.4. 条件表达式结构 结构图 语法说明 where: 条件表达式的标识; and:括号内的查询条件必须都为真; or:括号内的查询条件至少有一个为真; not:括号内的查询条件为假; 逻辑的连接祠是一种作用于查询条件的关键词。规则支持and、or和not三个关联词。 逻辑的连接词需要和括号一同使用,括号中为具体的查询条件。其中,and和or可以作用于多个查询条件(即括号中可以包含多个查询条件,以逗号分隔开),not只能作用于一个查询条件。 逻辑连接词是一类作用于查询条件的特殊运算符(普通的运算符作用于节点或属性) 逻辑的连接词作用的查询条件也可以包含逻辑连接词。 示例 /* * 问题检查条件: * - 查找函数名为 debug 或 foo; * - 并且(And): 参数数量为 1 或 3。 */ functionDeclaration fd where and(or(fd.name == "debug", fd.name == "foo"), or(fd.parameters.size() == 1, fd.parameters.size() == 3)); 2.5. 条件判断(condition) 结构图 2.5.1. 运算符 查询条件通过运算符作用于具体的节点或属性从而完成节点筛选。 根据运算符作用的属性类别,我们可以将运算符分为: 通用运算符 算术运算符 字符串运算符 布尔运算符 节点运算符 运算符的左值为:节点或节点的属性。 2.5.1.1. 通用运算符(==, contain) 等于(==)、不等于(!=) 数字 运算符右值为:整数或小数。 例如:arguments.size() == 3 字符串 运算符右值为:字符串常量。 例如:name == “foo” 布尔 运算符右值为:布尔常量:true或false。 例如:isPublic == true 节点 运算符右值为:空常量null。 例如:initializer == null 包含(contain)、不包含(notContain) 字符串 运算符右值为:字符串,且包含时,返回真。 例如:recordDeclaration where name contain "debug";,返回函数名中包含"debug" 时,返回真。 节点 运算符右值为:子查询条件,当子查询返回的节点满足时,返回真。 例如:functionDeclaration where parameters contain param where param.name == "i1";,返回包含名为i1参数的方法声明。 2.5.1.2. 算术运算符(>, <) 算术运算符操作数字。 大于(>)、小于(<)、大于等于(>=)、小于等于(<=) 数字 运算符右值为:数字,且满足时,返回真。 2.5.1.3. 字符串运算符(startWith, endWith, match) 字符串运算符操作字符串。 字符串的开始字符串( startWith) 左值开始字符串等于右值字符串时,返回真。 例如:functionDeclartion where name startWith "debug";,筛选出名字以debug开头的方法声明。 字符串的结束字符串( endWith) 左值结束字符串等于右值字符串时,返回真。 例如:functionDeclartion where name endWith "hello",筛选出名字以hello结尾的方法声明。 字符串匹配( match) 左值符合右值的正则表达式时,返回真。 例如:functionDeclaration where name match ".*(login).*",筛选出名字匹配正则表达式.*(login).*的方法声明。 2.5.1.4. 布尔运算符(!) 逻辑运算符操作布尔值。 非( !) 操作符位于布尔属性的左侧,表示布尔值为false时,返回真。 例如:recordDeclaration where !isPublic等价于recordDelaration where isPublic == false。 2.5.1.5. 节点运算符(contain,in,is) 节点运算符的左侧可以是节点属性 或 别名。 包含( contain) 用于查询语法树结构上的子节点。 例如: 筛选出方法体内调用了名为foo的无参方法的方法声明。 functionDeclaration fd where fd contain functionCall where and(name == "foo", arguments.size() == 0)); 存在于( in) 查询语法树结构上的父节点。 例如: 筛选出位于赋值表达式中的数据成员访问节点。 fieldAccess fa1 where fa1 in assignStatement; 是( is) 右侧节点属性或别名相同时,返回真。 例如:筛选出出现在赋值表达式左侧的变量访问节点,且赋值表达式的右侧为方法调用节点。规则中的va是节点variableAccess的别名。 variableAccess va where va in assignStatement where and(lhs is va, rhs is functionCall); 2.5.2. 值类型 2.5.2.1. 布尔值 布尔值为布尔常量true或false。 布尔属性可以单独使用。单独使用时,布尔属性隐式等价于一个包含==的通用条件表达式。 例如,recordDeclaration where isPublic;等价于recordDeclaration where isPublic == true; 2.5.2.2. 数值 规则中的数值可以为整数或小数。 2.5.2.3. 字符串 字符串支持大小写英文字母、数字、以及正则表达式特殊字符。 2.5.2.4. 空值(null) 空值指的是源码中的null。 在使用时,空值往往和==或!=一同出现。 例如: // private String field = null fieldDeclaration fd where fd.initializer == null; // str = null assignStatement as1 where as1.rhs == null; 3. CodeNavi插件 在Vscode 的插件中,查询:codenavi,并安装。 点击关注,第一时间了解华为云新鲜技术~