你是否曾经为了让你的代码更标准、更易读、更契合最佳实践而苦恼过?假如你常常参加代码开发,那么你必定知道代码的标准性对于整个项目的成功十分重要。代码标准性不只能够进步代码的可读性,还能够减少出现bug的概率。

市面上有许多能够帮你主动查看和修正你的代码中的过错用法的东西了。可是假如想为你的团队建立贴合事务的定制规矩,就没有那么多简略好用的东西了。

假如你想要快速落地定制规矩话,那么你必定要了解一下ast-grep,一个根据笼统语法树(AST)的代码查找、lint和重写东西。让你用YAML,像写jQuery选择器相同写规矩! github.com/ast-grep/as…

为什么要用查看东西?

代码检测东西是一种能够协助开发者进步代码质量和功率的东西。它能够在编写或运转代码的过程中,主动查看和修正代码中的语法过错、风格不一致、潜在的逻辑问题等。 在团队中运用查看东西,有这些好处:

  • 能够依据事务需求定制代码的最佳实践,比方命名标准、注释标准、功能优化等,然后保证代码的可读性、可维护性和可扩展性
  • 能够共享更好的常识,比方运用一些新的言语特性、结构或许库,或许防止一些常见的过错和陷阱,然后进步开发者的技能和水平。
  • 能够减轻代码查看的负担,比方减少人为的失误、遗漏和冲突,或许供给一些主动化的建议和反馈,然后进步代码查看的功率和质量。

代码查看不只仅是进步了代码质量,提升了个人工作功率。更重要的是,它是一种团队协作东西,协助新员工融入团队工程,不必再翻找阅览团队长长的代码标准文档。东西能直接在他撰写代码时通过实时报错一点一滴帮成员建立标准和常识!

什么是AST?为什么用它?

一个代码检测东西一般都会运用根据笼统语法树(Abstract Syntax Tree)来进行代码剖析,一般咱们简称它为AST。

AST是一种用来表明代码结构和语义的树形数据结构,每个节点代表一个代码元素,如变量、函数、表达式等。AST能够协助咱们剖析和操作代码,而不需求关心代码的文本方法。比方,下面这段JavaScript代码:

var x = 1 + 2;

能够表达成下面的AST方法。

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "x"
          },
          "init": {
            "type": "BinaryExpression",
            "operator": "+",
            "left": {
              "type": "Literal",
              "value": 1
            },
            "right": {
              "type": "Literal",
              "value": 2
            }
          }
        }
      ],
      "kind": "var"
    }
  ]
}

一个YAML就能极速检测超大Repo?用低代码定制你的代码检测!

由于代码自身是文本,所以很自然地咱们会想要用文本处理东西(比方正则表达式)来处理代码。可是AST的剖析是优于文本剖析的。由于根据文本的剖析只能对代码的表面方法进行处理,而不能获取代码的语法和语义信息,比方节点类型、特点、联系等。这些信息能够协助检测东西找出代码中的过错和缝隙。别的,根据文本的剖析简单遭到代码的格式和风格的影响,比方空格、换行、注释等,而不能精确地辨认代码的结构和内容。这或许导致一些误报或漏报的情况。

为什么写AST东西那么麻烦?

尽管AST比较强大,可是遍历解析AST的工作却是比较繁琐又无趣的。

  • AST是源代码的笼统语法结构的树状表明,要写AST东西就要了解不同言语的语法规矩和AST结构,这需求必定的学习本钱。
  • AST的遍历算法尽管只有一种,可是完结起来还是有一些细节和变形,比方递归和循环、visitorKeys的抽离和写死、enter和exit的回调等,需求留意不同场景下的优缺点。
  • AST的操作也需求必定的技巧,比方怎么增删改查节点、怎么保持语法正确性、怎么处理异常情况等,需求对AST有深化的了解和掌握。

咱们拿eslint作为比方。 eslint是一个用于查看和修正JavaScript代码的东西,是一个用ast来完结的东西。eslint能够运用自定义的解析器来将源代码转换成ast,然后运用选择器来遍历和操作ast,然后完结对代码的查看和修正。你能够运用一些在线东西来查看和修正ast,比方esprima.org/demo/parse。

咱们来写一个匹配一切在for循环中运用await表达式的eslint规矩。依据,一个eslint的规矩是一个JavaScript模块,它导出一个方针,包括以下几个特点:

  • meta: 一个方针,包括规矩的元信息,比方文档,类型,过错信息等。
  • create: 一个函数,返回一个方针,定义了规矩怎么查看代码。这个方针的键是AST节点的类型,值是一个函数,承受一个节点作为参数,并依据需求陈述过错。

你能够参考中的一些比方来写你自己的eslint规矩。对于这个需求,你能够写一个这样的规矩:

module.exports = {
  meta: {
    type: "problem",
    docs: {
      description: "disallow await inside of loops",
      category: "Possible Errors",
      recommended: false,
      url: "https://eslint.org/docs/rules/no-await-in-loop"
    },
    messages: {
      unexpected: "Unexpected `await` inside a loop."
    },
    schema: []
  },
  create(context) {
    // A stack to hold the state of each loop
    const loopStack = [];
    return {
      // When entering a loop, push a new state into the stack
      ForStatement(node) {
        loopStack.push({ upper: context.getScope().upper });
      },
      ForInStatement(node) {
        loopStack.push({ upper: context.getScope().upper });
      },
      ForOfStatement(node) {
        loopStack.push({ upper: context.getScope().upper });
      },
      WhileStatement(node) {
        loopStack.push({ upper: context.getScope().upper });
      },
      DoWhileStatement(node) {
        loopStack.push({ upper: context.getScope().upper });
      },
      // When leaving a loop, pop the state from the stack
      "ForStatement:exit"(node) {
        loopStack.pop();
      },
      "ForInStatement:exit"(node) {
        loopStack.pop();
      },
      "ForOfStatement:exit"(node) {
        loopStack.pop();
      },
      "WhileStatement:exit"(node) {
        loopStack.pop();
      },
      "DoWhileStatement:exit"(node) {
        loopStack.pop();
      },
      // When encountering an await expression, check if it is inside a loop
      AwaitExpression(node) {
        const state = loopStack[loopStack.length - 1];
        if (state && state.upper === context.getScope().upper) {
          context.report({
            node,
            messageId: "unexpected"
          });
        }
      }
    };
  }
};

这个规矩运用了一个栈来保存每个循环的状态,当遇到await表达式时,查看它是否在循环内部。假如是,就陈述一个过错信息。 能够看到,eslint的代码行数比较多,写起来比较繁琐。阅览的时分也需求看完代码上下文才能了解规矩的意图。那有没有更方便的方法能完结呢?

有没有一个东西能帮咱们快速上手AST操作呢?咱们日常工作中最最常用并且最最好用的树结构,其实正是前端里的DOM树。在过去的开发中,几乎人人都能用jQuery写两段DOM操作,入门门槛十分低。那咱们能不能用DOM树的方法来操作AST呢?让一切人都能上手写两个简易的AST东西呢?乃至于能学习CSS选择器相同,让运用者不写逻辑,直接低代码装备AST操作?

有!那便是ast-grep!

什么是ast-grep?

ast-grep是一个根据AST的东西,能够用简略的代码形式来查找、lint和重写代码。你能够像写普通的代码相同写形式,它会匹配一切有相同语法结构的代码。你能够用$符号加大写字母作为通配符,比方$MATCH,来匹配恣意单个AST节点。想象一下正则表达式中的点.,只不过它不是根据文本的。你能够试试在线演示来感受一下!

ast-grep还供给了相似jQuery的思想模型来遍历AST,用YAML装备文件来编写新的lint规矩或许代码修正。ast-grep用Rust编写,根据tree-sitter进行解析,并利用多核并行处理,所以它的功能是杠杠的。此外它还有一个美丽的命令行界面:)

ast-grep的愿景是让笼统语法树的魔法遍及到每个人,并让人们从繁琐的AST编程中解放出来!假如你是一个开源库的作者,ast-grep能够协助你的库用户更简单地习惯破坏性改变。假如你是一个团队的技能负责人,ast-grep能够协助你制定和履行契合你事务需求的编码最佳实践。假如你是一个安全研究员,ast-grep能够协助你更快地编写规矩。

怎么安装和运用ast-grep?

你能够通过npm或许cargo来安装ast-grep。

# install via pnpm
npm install --global @ast-grep/cli
# install via cargo
cargo install ast-grep

形式匹配和查找

假定你有一个JavaScript项目,你想要找出一切运用了console.log的当地。你能够用ast-grep来查找这样的形式:

sg --pattern 'console.log($ARGS)' --language js
# or using shortcut
# sg -p 'console.log($ARGS)' -l js

这会匹配一切调用了console.log函数的代码,不论参数是什么。 在这儿,console.log($ARGS) 是一个形式代码,能够了解成一个代码的模板,只需方针代码的AST和这个模板匹配,便是一个命中。 $ARGS叫做元变量(metavariable),是一个通配符,能够匹配恣意的AST节点。因而console.log("hello world") 会被匹配到,console.log(greetings)也会。

你也能够在ast-grep的playground里试玩形式匹配!看看ast的形式匹配和正则表达式的形式匹配有什么不相同?

用YAML装备规矩

用形式匹配来查找代码尽管十分便利,可是它在匹配复杂代码的情况下简单显得绰绰有余。别的,单个形式既不满意复杂的逻辑需求,也不方便记录成文供团队共享协作。 因而,ast-grep供给了一种愈加灵敏的方法来编写规矩:用YAML装备文件。

一个ast-grep的yaml规矩文件有以下几个关键字段:

  • id: 一个仅有的,描述性的标识符,比方no-unused-variable。
  • message: 首要的信息,阐明为什么这个规矩被触发。它应该是单行的,简练的,可是满足详细,不需求额定的上下文。
  • severity: 指定匹配成果的等级。可选的有hint, info, warning, 或 error。
  • language: 指定要解析和匹配的言语。
  • rule: 一个方针,指定怎么找到匹配的AST节点。详见rule object reference。

它还有一些其他字段,咱们能够参考官网的文档。

咱们这儿写一个十分简略的规矩,匹配一切用eval函数的代码,并提示不安全。

id: no-eval
message: Avoid using eval function as it may cause security issues.
severity: warning
language: JavaScript
rule:
  pattern: eval($CODE)

以上的规矩中除了rule的其他字段是不言自明的,咱们讲解下rule这个字段。它接纳一个方针,在这儿咱们传入的方针有pattern字段。pattern字段的值便是咱们之前看到的模板代码,它会匹配一切对eval的直接调用。

咱们把它保存在一个名叫no-eval.yml的文件里,就能够用ast-grep的命令行来运转了!

sg scan -r no-eval.yml

这儿用到了ast-grep的scan 命令,它会按照供给的规矩来扫描当下目录的文件。-r的命令行参数是--rule的缩写,它指定了要运用的规矩文件。

当运转之后,咱们就能看到查看成果。

一个YAML就能极速检测超大Repo?用低代码定制你的代码检测!

当然,你也能够用web playground在线预览成果。

规矩进阶

以上只是最简略的写法!ast-grep的yaml装备还有更强大的装备功能!

在规矩中的rule承受一个方针,它能够有以下几种类型的键:

  • 原子规矩键:pattern, kind, regex。这些键用来匹配单个的AST节点。
  • 联系规矩键:inside, has等。这些键用来依据节点相对于其他节点的位置来过滤匹配。
  • 复合规矩键:all, any, not等。这些键用来用逻辑运算符组合其他规矩

咱们再写一个复杂点的比方。 假定你想要匹配一切在for循环中运用await表达式的代码,并给出一个提示信息。你能够写一个这样的yaml规矩:

id: no-await-in-for-loop
message: Avoid using await expression in for loop as it may cause performance issues.
severity: hint
language: JavaScript
rule:
  all:
    - pattern: await $PROMISE
    - inside:
        any:
          - kind: for_in_statement
          - kind: for_statement
          - kind: while_statement
          - kind: do_statement
      stopBy: end

这个规矩运用了all和inside两个复合规矩键,以及pattern和kind两个原子规矩键。它的含义是:匹配一切既是await表达式,又在for循环中的节点。

  • kind 承受一个AST节点的类型,你能够用Web Playground来找代码的节点类型。这儿提到的kind都是JS循环的语法类型。
  • all 承受一个规矩列表,假如一切的子规矩都匹配,那么all就匹配。
  • any 承受一个规矩列表,假如恣意一个的子规矩都匹配,那么any就匹配。
  • inside 承受一个联系规矩方针,假如节点在另一个满意联系规矩方针的节点内部,那么inside就会匹配。

例如,在上面的比方中,all的作用是要求节点既满意pattern规矩,又满意inside规矩。pattern规矩代表了需求匹配一切的await句子,而inside的作用是要求节点在任何一种for循环中。

你也能够用在线Playground来体会这个规矩。

总结

ast-grep是一个根据AST的代码查找和重写东西,它能够让你用YAML来编写代码查看的规矩,而不需求学习eslint的JavaScript写法。

YAML的规矩愈加简练和直观,能够用声明式的方法表达你想要匹配和替换的代码形式。

比较eslint,ast-grep的规矩更简单阅览和了解,并且功能更好,由于它运用了tree-sitter来解析代码,并利用了多核处理。

假如你需求自定义代码查看的规矩,ast-grep能让你用一个YAML就解决问题。你能够把YAML放在你的项目里,也能够放在npm包里,在代码提交时用sg命令运转一下就能做到检测。乃至能够把YAML存在数据库里,做成代码检测的低代码渠道。

YAML在手,天下我有。想了解更多低代码检测的高级用法,能够猛戳ast-grep官网。