首页
/ Flutter Engine Clang-Tidy 静态检查体系:.clang-tidy 配置、CI 分片与 FLUTTER_LINT_PRINT_FIX 启用新规则流程

Flutter Engine Clang-Tidy 静态检查体系:.clang-tidy 配置、CI 分片与 FLUTTER_LINT_PRINT_FIX 启用新规则流程

2026-09-06 16:02:36作者:范靓好Udolf

Flutter 引擎(Engine)自 2020 年 5 月起将 clang-tidy 纳入 CI 流水线,从仅检查代码格式升级为具备语义级检查能力的静态分析体系。本文基于仓库内 docs/engine/ci/Engine-Clang-Tidy-Linter.md 这份官方文档,结合引擎仓库中的检查脚本 engine/src/flutter/ci/clang_tidy.sh、Dart 封装工具 engine/src/flutter/tools/clang_tidy/lib/clang_tidy.dart 以及根配置 engine/src/flutter/.clang-tidy,完整讲解:本地如何运行引擎 linter、.clang-tidy 中启用了哪些检查及命名规范、FLUTTER_NOLINT 豁免机制的源码级规则、CI 四路运行与跨平台分片(sharding)原理,以及官方推荐的“借助 CI 自动打印补丁来落地大型新 lint 规则”的六步工作流。

背景:为什么引擎要引入 clang-tidy

在引入 clang-tidy 之前,Flutter 引擎在 CI 中唯一的 lint 检查是代码格式(formatting),没有任何语义层面的检查。引入 clang-tidy 后,代码开始被检查潜在的 bug、C++ 现代化用法、命名一致性、性能反模式等,但代价是:既有存量代码并不天然符合全部规则,因此需要一段“迁移期”,通过文件级豁免标记逐步消化历史违规。

这决定了本体系两个核心机制:

  1. 检查项是可协商的:启用哪些 check 并非一成不变,团队允许通过讨论增删;
  2. 豁免必须可追溯:文件级豁免不能随意写,必须关联 issue 跟踪(见下文 FLUTTER_NOLINT 格式要求)。

本地运行 linter

通过 CI 脚本运行

官方文档给出的本地运行方式是执行 flutter/ci/clang_tidy.sh。在当前仓库(引擎源码已并入 monorepo 的 engine/src 下)中,对应文件为 engine/src/flutter/ci/clang_tidy.sh。该脚本做了三件事:

  1. 定位 Dart SDK 并确定架构:通过 dart_bin()third_party/dart/tools/sdks/dart-sdk/bin 找到 dart 可执行文件;若系统 archarm64,则显式指定 flutter/buildtools/mac-arm64/clang/bin/clang-tidy 路径(脚本第 57-60 行);
  2. 确保构建产物存在:clang-tidy 依赖 compile_commands.json。脚本检查 out/host_debug/compile_commands.json 是否存在,若缺失则先执行 ./flutter/tools/gn 生成(脚本第 62-65 行)——这就是文档中“Before the linter can run, the target must be built in order to generate code(linter 运行前必须先构建目标以生成代码信息)”的落地逻辑;
  3. 委托 Dart 工具执行:最终调用 dart $SRC_DIR/flutter/tools/clang_tidy/bin/main.dart --src-dir=...,并把命令行透传参数 $@ 原样转发(脚本第 69-75 行)。

脚本还包含一个关键开关(第 44-55 行):

# FLUTTER_LINT_PRINT_FIX will make it so that fix is executed and the generated
# diff is printed to stdout if clang-tidy fails. This is helpful for enabling
# new lints.

# To run on CI, just uncomment the following line:
# FLUTTER_LINT_PRINT_FIX=1

FLUTTER_LINT_PRINT_FIX 被设置时,脚本向 Dart 工具追加 --fix --lint-all 参数;一旦检查失败,脚本会打印 git diff 输出由 clang-tidy --fix 生成的补丁(第 77-85 行)。这一机制正是后文“在 CI 上批量启用大型新 lint”流程的核心。

直接使用 Dart 封装工具

脚本之外,也可以直接调用底层 Dart 工具(假设当前目录为引擎仓库根目录,即包含 tools/clang_tidy 的目录):

dart ./tools/clang_tidy/bin/main.dart

工具 README 的说明,默认只检查“最近一次构建”中“被修改过”的文件:修改范围由 git diff(对比 HEAD)确定,“最近一次构建”取 src/out/ 下最后更新的目录。常用能力包括:

# 应用自动修复
dart ./tools/clang_tidy/bin/main.dart --fix

# 临时增加 .clang-tidy 中未声明的检查项(支持通配符)
dart ./tools/clang_tidy/bin/main.dart --checks="<check-name-to-run>"
dart ./tools/clang_tidy/bin/main.dart --checks="readability-*"

# 移除某个检查项
dart ./tools/clang_tidy/bin/main.dart --checks="-<check-name-to-remove>"

# 清空所有检查项、只跑某一个
dart ./tools/clang_tidy/bin/main.dart --checks="-*,<only-check-to-run>"

# 指定引擎构建变体
dart ./tools/clang_tidy/bin/main.dart --target-variant android_debug_unopt

# 检查整个仓库(新增 lint 规则时使用)
dart ./tools/clang_tidy/bin/main.dart --lint-all

# 按路径正则过滤要检查的文件
dart ./tools/clang_tidy/bin/main.dart --lint-regex=".*test.*\.cc"

需要警惕的是:--lint-all 或宽泛的正则可能匹配成千上万个文件,README 中明确警告耗时可能达 30 分钟以上并长时间占用机器。

.clang-tidy 配置:启用的检查项与命名规范

检查项的权威配置在 engine/src/flutter/.clang-tidy(YAML 格式,- 前缀表示排除)。当前启用的检查项可分为几类:

Checks: >-
  bugprone-argument-comment,
  bugprone-use-after-move,
  bugprone-unchecked-optional-access,
  clang-analyzer-*,
  clang-diagnostic-*,
  darwin-*,
  google-*,
  modernize-use-default-member-init,
  objc-*,
  -objc-nsinvocation-argument-lifetime,
  readability-identifier-naming,
  -google-build-using-namespace,
  -google-default-arguments,
  -google-objc-global-variable-declaration,
  -google-objc-avoid-throwing-exception,
  -clang-analyzer-nullability.NullPassedToNonnull,
  -clang-analyzer-nullability.NullablePassedToNonnull,
  -clang-analyzer-nullability.NullReturnedFromNonnull,
  -clang-analyzer-nullability.NullableReturnedFromNonnull,
  -clang-analyzer-nullability.NullableDereferenced,
  performance-for-range-copy,
  performance-inefficient-vector-operation,
  performance-move-const-arg,
  performance-move-constructor-init,
  performance-unnecessary-copy-initialization,
  performance-unnecessary-value-param

可以看出配置的取舍逻辑:

  • 全量纳入bugprone-* 三个具体项、静态分析器全家桶 clang-analyzer-*、编译期诊断 clang-diagnostic-*、Apple 平台规则 darwin-*objc-*,以及 Google 风格规则 google-*
  • 选择性排除google-* 下几个过严或不适配的项(google-build-using-namespacegoogle-default-arguments、两个 Objective-C 相关项),objc-* 下的 objc-nsinvocation-argument-lifetime,以及 clang-analyzer-* 中全部 5 个 nullability 相关检查;
  • 性能类:显式列出 6 个 performance-* 检查(range-copy、低效 vector 操作、移动 const 参数、移动构造初始化、多余拷贝初始化、值传递参数)。

CheckOptions:命名规范的机器化强制

配置中的 CheckOptions 段把 Flutter 引擎的 C++ 命名约定交给 readability-identifier-naming 检查自动强制执行:

CheckOptions:
  - key: modernize-use-default-member-init.UseAssignment
    value: true
  - key: readability-identifier-naming.EnumConstantCase
    value: "CamelCase"
  - key: readability-identifier-naming.EnumConstantPrefix
    value: "k"
  - key: readability-identifier-naming.GlobalConstantCase
    value: "CamelCase"
  - key: readability-identifier-naming.GlobalConstantPrefix
    value: "k"
  - key: readability-identifier-naming.PrivateMemberCase
    value: "lower_case"
  - key: readability-identifier-naming.PrivateMemberSuffix
    value: "_"

即:枚举常量与全局常量要求 k 前缀 + CamelCase(如 kImpellerFoo),成员变量要求 lower_case 加下划线后缀(如 pipeline_)。UseAssignment: true 则让 modernize-use-default-member-init 以赋值形式(int x = 0;)而非成员初始化器(int x{0};)生成修复建议。

HeaderFilterRegex:一个“抱歉但必须”的正则

配置的最后一行是整个文件里被注释吐槽最多的部分(原文注释自称 “tl;dr: I'm sorry.”):

HeaderFilterRegex: "[..\/]+\/flutter\/(assets|benchmarking|bin|build|ci|common|display_list|docs|examples|flow|flutter_frontend_server|flutter_vma|fml|impeller|lib|runtime|shell|skia|skwasm|sky|testing|tools|txt|vulkan|wasm|web_sdk)\/.*"

它的作用与困境在文件内注释中解释得非常直白:

  • 目标:检查 flutter/ 下所有头文件,但排除 gen/(生成代码无需通过 lint)和 third_party/(依赖代码,非本仓库产物);
  • 困境:clang-tidy 使用的正则引擎是“很古老的版本,不支持 lookahead(前瞻)”,所以无法写“除 third_party 外全部”的否定式匹配,只能把每个需要检查的目录逐一枚举出来;
  • 防退化保障:注释指出,如果未来在 flutter/ 下新增根目录,必须同步更新这条正则;为防止遗漏,engine/src/flutter/tools/clang_tidy/test/header_filter_regex_test.dart 中的测试会“理论上”在新增目录但正则未更新时捕获该不一致。

此外仓库中还存在两份按目录收窄的配置:engine/src/flutter/shell/platform/linux/.clang-tidyengine/src/flutter/third_party/.clang-tidy,clang-tidy 会按文件位置就近选取配置,这与 third_party/ 整体豁免的策略相呼应。

FLUTTER_NOLINT 豁免机制的源码级规则

文档层面只有两句话:文件顶部有 // FLUTTER_NOLINT 时,linter 会忽略该文件;问题修复后应删除该注释。但源码中的实际规则更严格,实现在 engine/src/flutter/tools/clang_tidy/lib/src/command.dart

static final RegExp _nolintRegex = RegExp(
  r'//\s*FLUTTER_NOLINT(: https://github.com/flutter/flutter/issues/\d+)?',
);

Command.getLintAction() 对每个文件做出五选一决策(LintAction 枚举):

决策 触发条件 行为
skipThirdParty 路径任一段为 third_party 直接忽略
skipMissing 文件本地不存在 直接忽略
skipNoLint 文件头部(第一段真实代码之前,即空白行///# 开头的行范围内)出现格式合法// FLUTTER_NOLINT: https://github.com/flutter/flutter/issues/<ISSUE_ID> 忽略该文件,日志打印 “ignoring ... (FLUTTER_NOLINT)”
failMalformedNoLint 出现了 FLUTTER_NOLINT没有附带 issue 链接(match.group(1) == null 使本次运行失败,并提示要求的格式
lint 未出现豁免标记 正常 lint

这里有一个比原文档更关键的细节:在当前源码实现中,不带 issue 链接的裸 // FLUTTER_NOLINT 不再只是“被忽略”,而是直接导致 CI 失败——豁免必须关联一个 issue 编号用于跟踪。主程序 engine/src/flutter/tools/clang_tidy/lib/clang_tidy.dart_computeJobs 会把 failMalformedNoLint 汇总进退出码(sawMalformed 为 true 时返回 1)。相关行为在 engine/src/flutter/tools/clang_tidy/test/clang_tidy_test.dart 中有测试覆盖,仓库内仍有少量历史文件带 FLUTTER_NOLINT 注释(如 impeller/compiler/reflector.cc),说明“修复后移除注释”的迁移过程仍在持续。

CI 运行模型:四路运行与分片原理

文档 “CI background information” 一节给出了三条事实,均可在源码中找到对应实现:

  1. clang-tidy CI 步骤运行 4 次:mac 上的 host_debug、mac 上的 ios_debug、linux 上的 host_debug、linux 上的 android_debug_arm64
  2. linter 运行前目标必须先构建——对应 engine/src/flutter/ci/clang_tidy.sh 中“缺失 compile_commands.json 则先跑 tools/gn”的逻辑;
  3. job 按文件交集分片:iOS 与 macOS 运行的文件交集是共享的,Linux 与 Android 同理。

第 3 条的分片实现位于 Dart 工具中。两个参数配合使用:

  • --shard-variants:逗号分隔的其他构建变体列表,指向各自 out/<variant>/compile_commands.json
  • --shard-id:本次运行是哪个分片(必须是 0shard-variants 数量 之间的整数,参数校验见 engine/src/flutter/tools/clang_tidy/lib/src/options.dart)。

核心算法在 clang_tidy.dart 的 getLintCommandsForFiles 中:

  1. 把本变体与所有 shard 变体的 compile_commands.json 分别解析为命令集与文件路径集合;
  2. 对每个命令计算集合状态——若其文件出现在所有 shard 变体中则为 Intersection(即两平台共享的文件,如 iOS/macOS 共用代码),否则为 Difference
  3. Difference 部分由本运行完整负责;Intersection 部分先按文件路径排序(保证 json 顺序不确定时切分仍稳定),再按 _takeShard 规则 f(n) = value(n * shardCount + shardId) 取模分配——每个分片只跑共享文件的一个不相交子集;
  4. 每个命令再叠加 FLUTTER_NOLINT/third_party/文件缺失的 LintAction 过滤。

由此形成文档描述的拓扑:mac 侧两个 job(host 与 ios)和 linux 侧两个 job(host 与 android)各自通过“差集全跑 + 交集切半”覆盖全部文件,而跨平台不做分片——这正是文档流程中“mac 运行与 linux 运行之间可能存在重叠补丁”的原因,人工合补丁时需要注意去重。

另一个值得注意的健壮性细节:_runJobs 中对退出码为 SIGSEGV 的 job 会打印 “Crash when running clang-tidy ... Skipping this file.” 并计入 ignoredFailures 而不判失败——clang_tidy.dart 的注释说明这是新版本 clang-tidy 处理部分引擎源文件时崩溃的临时规避(带 TODO 注明待上游修复后移除)。

CLI 参数全景:Options 层如何解析

options.dart_argParser 定义了完整参数面,除上文已覆盖的 --fix--checks--lint-all--lint-regex--lint-head--target-variant--src-dir--shard-id--shard-variants--clang-tidy 外,还有几个容易忽略的点:

  • --compile-commands <path>:直接指定 compile_commands.json 路径,用于对比两个引擎 checkout 等场景;与 --target-variant--src-dir 互斥(_checkArguments 中校验);
  • --lint-head:检查“tip-of-tree 提交中变更的文件”,与默认的 LintChanged(对比工作区相对 HEAD 的改动)、LintAllLintRegex 互斥,最多只能传一个;
  • 环境变量旁路FLUTTER_LINT_ALL 环境变量等同于 --lint-all(见 Options._fromArgResults 第 62 行);
  • --mac-host-warnings-as-errors:仅当 --target-varianthost_debug 且运行在 macOS 上时生效(_platformSpecificWarningsAsErrors),把指定 check 的 warning 提升为 error——注释说明其用途是“按平台逐个迁移时,只把某个平台设为硬失败”;
  • --enable-check-profile:启用 clang-tidy 的 per-check 计时并在 job 成功时把 profile 输出到 stderr,可用于分析哪些 check 最耗时;
  • --warnings-as-errors 的默认值createLintJob 中当选项为空时默认传 --warnings-as-errors=*command.dart),即默认所有 warning 都是 error——引擎 lint 没有“仅告警不阻塞”的宽松模式。

如何在 CI 上落地一个大型新 lint:FLUTTER_LINT_PRINT_FIX 六步流程

这是原文档最具实操价值的部分,完整继承其步骤(原文路径 //ci/clang_tidy.sh 在当前仓库对应 engine/src/flutter/ci/clang_tidy.sh):

准备:两条能显著提高成功率的经验(原文 Tips)

  • 一次少开几个 check:多个 check 同时开启会产生级联效应——用 clang-tidy --fix 修好一个 check 的违规,可能引入其他 check 的违规,分批推进更容易收敛;
  • 优先用 NOLINTNEXTLINE 而非 NOLINT:自动格式化(formatter)可能移动 NOLINT 注释导致其失效,NOLINTNEXTLINE 不受此影响。

六步流程

  1. 编辑 ci/clang_tidy.sh,取消打印修复开关的注释:

    # To run on CI, just uncomment the following line:
    -# FLUTTER_LINT_PRINT_FIX=1
    +FLUTTER_LINT_PRINT_FIX=1
    

    此后 CI 会对所有文件运行检查,并在失败时打印 clang-tidy --fix 生成的补丁(对应脚本中 --fix --lint-all 参数与结尾的 git --no-pager diff);

  2. 创建一个 draft PR,包含新增的 check 和 FLUTTER_LINT_PRINT_FIX=1

  3. 查看失败的 clang-tidy job 输出,确认补丁没有被“打花”(garble)——自动修复偶尔会产出坏补丁,遇到时需要手动修复打花的位置,或对那处改用 NOLINTNEXTLINE

  4. 把 CI bot 打印的补丁复制到剪贴板;在终端进入引擎仓库执行 git apply,粘贴补丁后按 ctrl+d 再回车。注意:由于 mac 与 linux 运行之间不做分片,两者的补丁可能存在重叠git apply 时需注意;

  5. 提交并推送;

  6. 等 4 路 CI 全部变绿后,移除 FLUTTER_LINT_PRINT_FIX=1,把 PR 转为正式评审。

这套流程的本质是:把“在本地跑 4 次(对应 4 个构建变体)生成修复”的成本转嫁给 CI bot,开发者只负责审查补丁、应用补丁、复核结果。

FAQ:官方给出的协作约定

原文档的三个 FAQ 值得保留为团队约定:

  1. 看不懂某个 lint 报错怎么办:到 hackers-engine Discord 频道求助(文档中提及可 ping 相关维护者);
  2. “为什么在/不在检查 X?”:启用的检查项是可协商的(negotiable),认为遗漏了某项检查应在 hackers-engine 讨论;
  3. 能否直接用 NOLINT 关掉报错?:可以,但必须先获得团队成员的明确批准(explicit approval)。

结合源码可以看到约定有双重保障:机器层面,不带 issue 链接的 FLUTTER_NOLINT 会被判为 malformed 并让 CI 失败(上一节所述);流程层面,文件级豁免需关联 issue 跟踪,确保“豁免”不是永久状态,而是有待偿还的迁移债务。

小结:一条完整的检查链路

把上述材料串起来,引擎代码从提交到被 clang-tidy 检查的完整链路是:

  1. CI 在 4 个(平台 × 构建变体)组合上运行 engine/src/flutter/ci/clang_tidy.sh,必要时先 tools/gn 生成 compile_commands.json
  2. 脚本调用 tools/clang_tidy/bin/main.dart,由 Options 解析分片、变体、检查项等参数;
  3. ClangTidy.getLintCommandsForFiles 基于 compile_commands.json 的交集/差集 + 取模分片计算本 job 的文件集,再经 FLUTTER_NOLINT 状态机过滤;
  4. 每个文件生成一条 clang-tidy 命令(把编译命令中的 clang 换成 clang-tidy,默认 --warnings-as-errors=*),并发执行;
  5. 失败输出经 trimOutput 裁剪后打印,退出码汇总;FLUTTER_LINT_PRINT_FIX=1 时额外打印 git diff 补丁供批量启用新 check。

对引擎贡献者而言,日常只需记住三个动作:改代码后默认只查改动文件;新增 lint 时走 --lint-all 加六步 CI 补丁流程;确需豁免时写带 issue 链接的 FLUTTER_NOLINT 并争取尽快移除。

登录后查看全文
热门项目推荐
相关项目推荐