Slang 旨在提升下一代 Solidity 代码分析和开发工具, 将展示如何使用 Slang 在仅 25 行代码中编写一个简单的 Solidity linter
Slang 是 Nomic Foundation 的一套模块化编译器 API,旨在提升下一代 Solidity 代码分析和开发工具。它是用 Rust 编写的,并以多种语言分发。目前处于 alpha 阶段并在积极开发中,但已经对许多事情有用!查看 初始 alpha 版本发布公告 了解更多信息。
在本指南中,我们将展示如何使用 Slang 在仅 25 行代码中编写一个简单的 Solidity linter。为了选择一个简单但真实的例子,我们将编写我们自己的 solhint [avoid-tx-origin](https://solhint-community.github.io/solhint-community/docs/rules/security/avoid-tx-origin.html)
规则版本,该规则在代码中使用 tx.origin
时发出警告。
官方 Solidity 文档包含一个示例,说明了为什么使用 tx.origin
进行授权是个坏主意:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract TxUserWallet {
address owner;constructor() {
owner = msg.sender;
}
function transferTo(address payable dest, uint amount) public {
// THE BUG IS RIGHT HERE, you must use msg.sender instead of tx.origin
require(tx.origin == owner);
dest.transfer(amount);
}
}
我们希望我们的 linter 能够简要报告有问题的代码位置:
example.sol:13:17: warning: avoid using `tx.origin`
与大多数 linter 手动遍历语法树以查找源代码模式不同,我们可以使用 Slang 的树查询语言。它允许我们简洁地指定我们正在寻找的模式,查询引擎将为我们完成所有繁重的工作!
为了让你了解我们的目标,以下是一个预览:
// Query that we’ll use to find the `tx.origin` expression
const query = Query.parse(
`@txorigin [MemberAccessExpression
... [Expression ... ["tx"] ...]
... [MemberAccess ... ["origin"] ...]
...
]`,
);// Parse the source code:
const language = new Language("0.8.22");
const output = language.parse(NonterminalKind.SourceUnit, contents);
// Query the parsed code...
const cursor = output.createTreeCursor();
const matches = cursor.query([query]);
// ...and print the results:
let match = null;
while ((match = matches.next())) {
const txorigin = match.captures.txorigin[0];
const { line, column } = txorigin.textOffset;
console.warn(`${filePath}:${line + 1}:${column + 1}: warning: avoid using \\`tx.origin\\`\`);
}
这正是我们想要的输出!让我们深入了解如何逐步实现这一目标。
首先,我们需要安装 Slang。编译器是用 Rust 编写的,并作为 Rust 包和带有 TypeScript 定义的 NPM 包分发。在本指南中,我们将使用后者。
让我们打开终端并创建一个新项目:
mkdir my-awesome-linter/
cd my-awesome-linter
npm init
npm install @nomicfoundation/slang
我们将使用 TypeScript 编写我们的 linter。让我们安装它并创建一个 tsconfig.json
文件:
npm install --save-dev typescript @types/node
npx tsc --init
为了分析代码,我们需要将其解析为具体语法树(CST)。CST 可以表示不完整或无效的代码,是编写 linter 的良好起点。
让我们从编写一个简单的 index.ts
开始,该文件读取作为第一个命令行参数指定的文件内容:
// index.ts
import fs from 'node:fs';
const filePath = process.argv[2];
const contents = fs.readFileSync(filePath, 'utf8');
Solidity 语言随着时间的推移发生了很大变化,然而 Slang 能够解析今天使用的所有 Solidity 版本,我们认为这些版本是 0.4.11 及更高版本。
假设我们希望与预期在 Solidity 0.8.22 上运行的代码兼容。首先,我们构造一个 Language
类的实例,这是解析 Solidity 代码的主要入口点:
import { Language } from "@nomicfoundation/slang/language";
const language = new Language("0.8.22");
要使用 Slang 解析文件,我们将使用 language.parse()
方法,该方法将 NonterminalKind
作为第一个参数,允许我们指定要解析的语言结构。由于我们要解析整个文件,因此我们将使用 NonterminalKind.SourceUnit
。
import { NonterminalKind } from "@nomicfoundation/slang/kinds";
const output = language.parse(NonterminalKind.SourceUnit, contents);
parse
函数返回一个 ParseOutput
对象,其中包含 CST 的根节点 (tree()
) 和解析错误列表 (errors()
),如果有的话。
为了检查 CST,我们可以将其打印到控制台以查看其外观:
const tree = results.tree();
console.log(tree.toJSON());
// Should print something like:
// {"kind":"SourceUnit","text_len":{...},"children":[...]}
我们刚刚将 Solidity 代码解析为一个结构化表示,现在我们可以对其进行分析。
为了分析 CST,我们将使用 Slang 的树查询语言,它专为像我们这样的任务而设计,是手动分析树的一个很好的替代方案,因为它简洁且具有声明性。
树查询是 Query
类的实例,通过解析查询字符串创建,这些字符串匹配特定的 CST 模式,并可选地将变量绑定到它们。语法在 Tree Query Language 参考中进行了描述。
不深入探讨此查询的细节,我们希望匹配 tx.origin
表达式,这是一个 MemberAccessExpression
,其左侧是 tx
标识符,右侧是 origin
标识符:
import { Query } from "@nomicfoundation/slang/query";
let query = Query.parse(
`@txorigin [MemberAccessExpression
... [Expression ... @start ["tx"] ...]
... [MemberAccess ... ["origin"] ...]
...
]`,);
这里有很多内容需要解读!让我们分解一下:
[]
括起来。NonterminalKind
。...
是一个通配符,匹配任意数量的子节点。@
前缀名称是 捕获,用于引用匹配模式的特定节点。查询是使用 Cursor
类执行的,这是遍历语法树的另一种方式,因此我们需要实例化一个从树的根节点开始的游标:
const cursor = results.createTreeCursor();
// 这是以下代码的简写:
// results.tree().createCursor({ utf8: 0, utf16: 0, line: 0, column: 0 })
虽然可以使用同一个 cursor 并发运行多个不同的查询,但在我们的例子中,我们只运行一个:
const matches = cursor.query([query]);
要访问匹配的 QueryResult
,我们需要重复调用 next()
直到它返回 null
:
let match = null;
while (match = matches.next()) {
// ... 对匹配的树片段做一些处理
}
现在,对于每个查询结果,我们可以使用查询中定义的捕获来访问我们感兴趣的节点。
每个 cursor 指向一个节点,但捕获可以根据查询返回多个 cursor。在我们的例子中,@txorigin
将返回一个指向 MemberAccessExpression
节点的 Cursor
数组。
让我们检查由 Cursor
指向的匹配节点的 JSON 表示:
const txorigin = match.captures.txorigin[0];
console.log(txorigin.node().toJSON());
// 应该打印出我们匹配的节点:
// {"kind":"MemberAccessExpression","text_len":{...},"children":[...]}
剩下的唯一事情就是向用户报告我们的发现。
因为我们从查询中得到了一个指向有问题节点的 Cursor
,我们可以使用它的 .textOffset
属性将其位置映射回源代码。此属性包含 .line
和 .column
属性,正是我们所需要的。值得注意的是,Slang 使用基于 0 的索引,但错误报告/编辑器通常使用更自然的基于 1 的索引,因此我们需要在这些偏移量上加 1
以使用它。
有了这些,我们可以打印出警告信息,通知用户有问题的代码在哪里:
const txorigin = match.captures.txorigin[0];
const { line, column } = txorigin.textOffset;
console.warn(`${filePath}:${line + 1}:${column + 1}: warning: avoid using \`tx.origin\``);
要访问节点的完整范围,我们可以使用 cursor 上的 textRange
属性,它返回节点在源代码中的起始和结束偏移量。
我们可以更有创意地将这些信息插入我们选择的自定义格式器中,但目前这就足够了。
以下是我们 linter 的完整代码:
// file: index.ts
import fs from "node:fs";
import { Language } from "@nomicfoundation/slang/language";
import { NonterminalKind } from "@nomicfoundation/slang/kinds";
import { Query } from "@nomicfoundation/slang/query";const filePath = process.argv[2];
const contents = fs.readFileSync(filePath, "utf8");
const language = new Language("0.8.22");
const output = language.parse(NonterminalKind.SourceUnit, contents);
const query = Query.parse(
`@txorigin [MemberAccessExpression
...
[Expression ... ["tx"] ...]
...
[MemberAccess ... ["origin"] ...]
]`,
);
const cursor = output.createTreeCursor();
const matches = cursor.query([query]);
let match = null;
while ((match = matches.next())) {
const txorigin = match.captures.txorigin[0];
const { line, column } = txorigin.textOffset;
console.warn(`${filePath}:${line + 1}:${column + 1}: warning: avoid using \`tx.origin\``);
}
如果我们不计算空行,代码确实只有 25 行! 🎉
在本指南中,我们演示了如何使用 Slang 为 Solidity 创建一个简单的 linter,实现了 solhint
中 avoid-tx-origin
规则的简单版本,仅用了 25 行代码。
我们涵盖了解析 Solidity 代码的基本知识,识别特定代码模式,并以清晰简明的方式向用户报告发现。
我们希望本指南能激发你编写自己的 linter 或任何其他使用 Slang 操作 Solidity 代码的工具!
如果你有任何问题或反馈,请随时在 GitHub 上联系我们,或查看 Slang 的 文档。
本文由 AI 翻译,欢迎小伙伴们来校对。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!