为 Fuchsia 创建静态分析器

Shac(可扩展封闭分析和检查)是一种统一的人体工程学工具, 用于编写和运行静态分析检查的框架。该工具的来源 可在 shac-documentation 中找到。Shac 检查的编写语言 Starlark

设置

Shac 脚本实现位于 Fuchsia 的 //scripts/shac 目录中。

  • Shac 检查以 starlark 函数的形式实现,该函数接受 ctx 参数 参数。使用此 ctx 参数访问 shac 标准库。
  • 如果您的检查结果因语言而异,则应采用相应语言 特定文件(例如:rust.stargo.starfidl.star)。如果是语言 但不包含 language.star 文件,则创建一个。如果是宽泛的 使用 title.star(其中 title 是 check 函数的名称)。

简单示例

以下示例是针对所有文件的静态分析器,可创建 非阻塞、Gerrit 警告注释,用于对字符串“http://” ,从而将用户引导至“https://”。

def http_links(ctx):
    for path, meta in ctx.scm.affected_files().items():
        for num, line in meta.new_lines():
            matches = ctx.re.allmatches(r"(http://)\w+", line)
            if not matches:
                continue
            for match in matches:
                ctx.emit.finding(
                    message = "Avoid http:// links, prefer https://",
                    # Change to "error" if the check should block presubmit.
                    level = "warning",
                    filepath = path,
                    line = num,
                    col = match.offset + 1,
                    end_col = match.offset + 1 + len(match.groups[1]),
                    replacements = ["https://"],
                )

详细了解 Shac 的 emit.findings 实现。

load("./http_links.star", "http_links")  # NEW

...

def register_all_checks():
    ...
    shac.register_check(http_links)  # NEW
    ...

高级示例

如果已有工具执行检查或 如果检查逻辑很复杂(例如,不仅仅是子字符串搜索)。 Starlark 有意地限制了功能,以鼓励编写复杂的 业务逻辑。

以下是在单独的 Python 中实现的 JSON 格式设置工具的示例 脚本并将其作为子进程运行。

该检查不会重写格式有误的文件,而是会计算 并将其传递给replacements ctx.emit.finding() 函数。所有格式检查都必须在 原因如下:

  • 通过检查运行的子进程不得向检出中的文件写入数据 目录。这可以防止运行异常的工具进行意外更改,以及 确保可以安全地并行运行多项检查,而不会有竞态风险 条件。(请注意,仅在 Linux 上强制执行文件系统沙盒)。
  • Shac 旨在与需要 向用户提议更改(例如在 Gerrit 中),而不是自动 因此,要使这些用例生效, 传递给 Shac,而不是由子进程应用
import json
import sys


def main():
    # Accepts one positional argument referring to the file to format.
    path = sys.args[1]
    with open(path) as f:
        original = f.read()
    # Always use 2-space indents and a trailing blank line.
    formatted = json.dumps(json.loads(original), indent=2) + "\n"
    if formatted == original:
        sys.exit(0)
    else:
        print(json.dumps(doc, indent=2) + "\n")
        sys.exit(1)


if __name__ == "__main__":
    main()
load("./common.star", "FORMATTER_MSG", "cipd_platform_name", "get_fuchsia_dir", "os_exec")

def json_format(ctx):
    # Launch processes in parallel.
    procs = {}
    for f in ctx.scm.affected_files():
        if not f.endswith(".json"):
            continue
        # Call fuchsia-specific `os_exec` function instead of
        # `ctx.os.exec()` to ensure proper executable resolution.
        # `os_exec` starts the subprocess but does not block.
        procs[f] = os_exec(ctx, [
            "%s/prebuilt/third_party/python3/%s/bin/python3" % (
                get_fuchsia_dir(ctx),
                cipd_platform_name(ctx),
            ),
            "scripts/shac/json_format.py",
            f,
        ])

    for f, proc in procs.items():
        # wait() blocks until the process completes.
        res = proc.wait()
        if proc.retcode != 0:
            ctx.emit.finding(
                level = "error",
                filepath = f,
                # FORMATTER_MSG is the standard message for formatters
                # in fuchsia.git.
                message = FORMATTER_MSG,
                # json_format.py prints the formatted file contents to stdout.
                # Passing it to `replacements` is necessary for shac to know
                # how to apply the fix.
                replacements = [res.stdout],
            )

# TODO: call this somewhere
shac.register_check(shac.check(
    json_format,
    # Mark the check as a formatter. Only checks with `formatter = True`
    # get run by `fx format-code`.
    formatter = True,
))
性能优化

某些格式设置工具内置支持验证许多 同时在内部并行处理多个文件 这比启动单独的子进程来检查每个文件要快得多。在此示例中 您可以在“检查”中所有文件上运行一次格式化程序来获取 然后只对格式有误的文件进行迭代 获取带格式的结果(而不是遍历所有文件)。

示例:对于 rustfmt,请先运行 rustfmt --check --files-with-diff <all rust files> 以获取格式有误的文件列表,然后运行 rustfmt 以获取经过格式化的结果。

如果格式化程序没有试运行模式,用于将格式化的结果输出到 stdout:格式设置工具子进程将无法写入结账流程。 但是,有些格式设置工具会无条件地写入文件。在这种情况下 将每个文件复制到子进程可以写入的 tempdir 中, 临时文件,并报告其内容,有关示例,请参阅 buildifier

默认情况下,如果子进程生成子进程,os_exec 会引发不可恢复的错误 非零返回代码。如果需要非零返回代码,您可以使用 ok_retcodes 参数,例如ok_retcodes = [0, 1] 可能适合 当文件未设置格式时,formatter 会生成返回代码 1。

在本地运行的检查

在本地检查开发期间,建议通过运行以下命令来测试检查 Shac 是通过 fx host-tool shac check <file> 直接实现的。我们以 我们可以测试上述 http_links 检查:

  1. 查找目前违反了检查的文件;如果有,请创建一个新文件 不存在,例如:echo "http://example.com" > temp.txt
  2. fx host-tool shac check --only http_links temp.txt
    • 这应该会失败,并输出以“http://”开头的文件内容突出显示
    • --only 会导致 Shac 仅运行 http_links 检查,不包括其他 因为在这种情况下我们只关心测试 http_links 和 不关心其他检查的结果
  3. fx host-tool shac fix --only http_links temp.txt 应更改 http:// 至 https://
  4. fx host-tool shac check --only http_links temp.txt现在应该会通过
  5. fx host-tool shac check --only http_links --all
    • 针对树中的所有文件运行(在 //shac.textproto),而不仅仅是已更改的文件
    • 如果此操作失败并报错,那么您需要在 违规文件出现在同一提交中或单独的提交中 (如果修复的文件超过 10 个,则比较合适) 检查。
      • 或者,将检查设为非阻塞状态,修正错误 将其切换为“屏蔽”
    • 如果您的检查发出了警告,请记下警告数量。如果有 是一个非常大的数字(超过 100 秒),这会导致许多噪声 Gerrit 评论,并且可能会对其他贡献者造成干扰。您可以考虑将 从而缩小检查范围或重新考虑 实用性。
  6. 最后,将检查上传到 Gerrit,运行预提交,检查失败情况 目标为 0 次失败。(提交前的行为与运行 fx host-tool shac check --all 的行为相同)

建议您记录下您的检查,以确定它是否被选择加入(未在提交前运行)或存在不明显的 选择停用机制。所有文件都应添加到 //docs/development/source_code/presubmit_checks.md