建立 Fuchsia 的靜態分析工具

Shac (可擴充的密封分析與檢查) 是一套符合人體工學的整合式工具與架構,用於撰寫及執行靜態分析檢查。您可以在 shac-說明文件 中找到這項工具的來源。Shac 檢查是以 Starlark 格式編寫。

設定

Shac 指令碼實作位於 Fuchsia 的 //scripts/shac 目錄中。

  • shac 檢查是以星號函式的形式實作,以接受 {1/} 引數。使用這個 ctx 引數存取 shac 標準程式庫。
  • 如果檢查使用的是特定語言,則應採用其中一種特定語言的檔案 (例如:rust.stargo.starfidl.star)。如果特定語言專用,但沒有 language.star 檔案,請建立一個。如果是一般項目,請使用 title.star (其中標題為檢查函式的名稱)。

簡易範例

以下範例是對所有檔案建立的靜態分析工具,會在含有「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 格式器範例,並以子程序的形式執行。

檢查不會重新編寫格式錯誤的檔案,而是會計算格式化內容,並將其傳遞至 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:格式設定工具子程序將無法寫入結帳畫面。不過,部分格式設定工具無條件寫入檔案。在這種情況下,您需要將每個檔案複製到暫存空間,子程序可寫入、設定暫存檔案的格式並回報其內容,如「建構工具」一節所示。

根據預設,如果子程序產生非零的傳回代碼,os_exec 會發出無法復原的錯誤。如果預期傳回代碼為零,您可以使用 ok_retcodes 參數。舉例來說,如果格式化工具在檔案未設定格式時產生傳回代碼 1,可能適合使用 ok_retcodes = [0, 1]

在本機執行檢查

在本機檢查開發期間,建議您透過 fx host-tool shac check <file> 直接執行 shac,藉此測試檢查。讓我們建立一個情境,用來測試上述的 http_links 檢查:

  1. 尋找目前違反檢查的檔案,如果沒有檔案,則建立新的檔案,例如:echo "http://example.com" > temp.txt
  2. fx host-tool shac check --only http_links temp.txt
    • 這麼做應該會失敗,並輸出醒目顯示「http://」的檔案內容
    • --only 只會執行 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 中的 git-ignored 或忽略),不只是變更的檔案
    • 如果這項作業因錯誤而失敗,您必須在相同的修訂版本中或個別修訂版本中修正違規檔案中的這些錯誤 (建議修正檔案數量超過 10 個時建議使用)。
      • 或者,將檢查設為非阻斷狀態,修正錯誤,然後切換為封鎖
    • 如果檢查發出警告,請記下警告數量。如果數量非常龐大 (超過 100 秒),則將產生許多雜訊過多的留言,並可能會對其他貢獻者造成乾擾。請考慮事先進行大量修正、縮減檢查範圍,或重新考慮檢查是否的實用性。
  6. 最後,請將檢查上傳至 Gerrit、執行預先提交,然後檢查失敗的目標 0 次。(預先提交的行為與執行 fx host-tool shac check --all 相同)

建議您提供檢查程序,確認該選項是否為選擇採用 (不得在提交前執行),或是有明顯的選擇退出機制。所有說明文件都應該新增至 //docs/development/source_code/presubmit_checks.md