Shac(可扩缩的密封分析和检查)是一种统一且符合人体工程学的工具和框架,用于编写和运行静态分析检查。您可以在 shac-documentation 中找到该工具的源代码。Shac 检查使用 Starlark编写。
设置
Shac 脚本实现位于 Fuchsia 的 //scripts/shac 目录中。
- Shac 检查以接受 ctx 实参的 starlark 函数的形式实现。使用此 ctx 实参可访问 Shac 标准库。
- 如果您的检查是特定于语言的,则应将其放在其中一个特定于语言的文件中(例如:
rust.star、go.star、fidl.star)。如果它是特定于语言的,但没有language.star文件,请创建一个。如果是通用的,请使用title.star(其中 title 是检查函数的名称)。
简单示例
以下示例是一个针对所有文件的静态分析器,它会在存在字符串“http://”的更改中创建非阻塞的 Gerrit 警告注释,并引导用户改用“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 实现。
请注意,Shac 不会自动发现检查。如需运行检查,必须在 //scripts/shac/main.star 中将检查函数传递给
shac.register_check():
load("./http_links.star", "http_links") # NEW
...
def register_all_checks():
...
shac.register_check(http_links) # NEW
...
在已包含其他检查的文件中实现新检查时,您或许可以在该文件中注册新检查。例如,//scripts/shac/fidl.star 有一个
register_fidl_checks() 函数,该函数从 //scripts/shac/main.star 调用。将新的 FIDL
检查添加到 fidl.star,并在同一文件中的 register_fidl_checks() 函数中注册这些检查。
高级示例
如果存在执行检查的现有工具,或者检查的逻辑很复杂(例如,不仅仅是子字符串搜索),则使用子进程会很有用。 Starlark 有意限制了功能,以鼓励在具有自己单元测试的独立工具中编写复杂的业务逻辑。
以下示例是一个在单独的 Python 脚本中实现并作为子进程运行的 JSON 格式化程序。
该检查不会重写格式错误的文件,而是计算格式化后的内容,并将其传递给 ctx.emit.finding() 函数的 replacements
实参。所有格式化检查都必须以这种方式实现,原因如下:
- 检查运行的子进程不得写入检出目录中的文件。这可以防止行为不端的工具进行意外更改,并确保可以安全地并行运行多个检查,而不会出现竞态条件。(请注意,文件系统沙盒仅在 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 的试运行模式,则格式化程序子进程将无法写入检出。
不过,某些格式化程序会无条件写入文件。在这种情况下,您需要
将每个文件复制到子进程可以写入的临时目录中,格式化
临时文件并报告其内容,例如 buildifier。
默认情况下,如果子进程生成非零返回代码,os_exec 会引发不可恢复的错误。如果预期会出现非零返回代码,您可以使用
ok_retcodes 参数,例如,如果格式化程序在文件未格式化时生成返回代码 1,则 ok_retcodes = [0, 1] 可能适用。
在本地运行检查
如需在本地对更改中的修改后的文件运行静态分析检查,请使用 fx lint:
fx lint对修改后的文件运行所有树内 lint 工具和分析器。fx lint --fix会自动应用检查发出的建议替换。fx lint --only <check_name>仅运行特定检查。fx lint --all对代码库中的所有跟踪文件运行静态分析。
在本地检查开发期间,建议您通过运行
fx lint 或通过 fx host-tool shac check <file> 直接调用 Shac 来测试检查。让我们创建一个场景,以便测试上述 http_links 检查:
- 找到当前违反检查的文件,或者创建一个新文件(如果不存在),例如:
echo "http://example.com" > temp.txt fx host-tool shac check --only http_links temp.txt- 这应该会失败,并输出文件内容,其中“http://”突出显示
--only会导致 Shac 仅运行 http_links 检查,排除其他检查,因为在此实例中,我们只关心测试 http_links,而不关心其他检查的结果
fx host-tool shac fix --only http_links temp.txt应将 http:// 更改为 https://fx host-tool shac check --only http_links temp.txt现在应该通过fx host-tool shac check --only http_links --all- 在树中的所有文件(除了 git 忽略的文件或
//shac.textproto中忽略的文件)上运行,而不仅仅是更改的文件 - 如果此操作失败并出现错误,则需要在提交之前在问题文件中修复这些错误,可以在同一提交中修复,也可以在单独的提交中修复(如果要修复的文件超过 10 个,最好采用单独提交的方式)。
- 或者,将检查作为非阻塞检查提交,修复错误,然后将其切换为阻塞检查
- 如果您的检查发出警告,请注意警告的数量。如果数量非常大(超过 100 个),则会导致许多嘈杂的 Gerrit 注释,并可能会对其他贡献者造成干扰。请考虑事先进行批量修复,缩小检查范围或重新考虑检查的实用性。
- 在树中的所有文件(除了 git 忽略的文件或
- 最后,将检查上传到 Gerrit,运行预提交,检查失败情况,目标是 0 个失败。(预提交的行为与运行
fx host-tool shac check --all相同)
如果您的检查是选择性加入的(不在预提交中运行),或者存在不明显的选择退出机制,建议您记录检查。所有文档都应添加到
//docs/development/source_code/presubmit_checks.md