Flint++:C++代码质量守护者,从内存管理到现代化重构
1. 项目概述为什么我们需要Flint如果你写过C尤其是维护过一些历史遗留的代码库大概率会对“代码质量”这四个字有切肤之痛。我说的不是简单的语法错误而是那些更深层次、更隐蔽的问题内存泄漏的幽灵在运行时徘徊、指针像野马一样四处乱窜、复杂的类继承关系让代码像一团乱麻、以及那些看似无害但实则违背了现代C最佳实践的“老式”写法。这些问题编译器通常不会报错静态分析工具可能也因其规则宽泛而放过但它们就像代码里的“慢性病”日积月累终将导致项目难以维护、性能低下甚至崩溃。这就是Flint出现的背景。它不是另一个编译器也不是一个简单的代码格式化工具。Flint是一个专注于C的、轻量级但功能强大的源码分析工具。它的核心目标是像一个经验丰富的代码审查员自动扫描你的代码找出那些不符合最佳实践、存在潜在风险、或者可以优化得更好的“代码异味”。想象一下你刚写完一个模块运行flint your_file.cpp它就能给你一份详细的报告告诉你“嘿这里有个new没有配对的delete建议用智能指针”“看这里这个类拷贝构造函数和拷贝赋值运算符没定义小心浅拷贝陷阱”“这个函数参数是std::string但你没修改它改成const std::string能避免一次不必要的拷贝”。对于个人开发者它是提升代码质量的私人教练对于团队它是统一编码规范、保证代码库健康度的守门员。尤其是在当前C生态中从传统的C98/11向更现代的C17/20/23演进时Flint能帮助你快速识别并升级那些过时的写法让代码更安全、更高效、更清晰。2. 核心功能与设计理念拆解Flint的设计哲学非常明确快速、精准、可配置。它不追求大而全而是希望在常见的、关键的代码质量问题上给出立即可行的建议。2.1 核心检测能力剖析Flint的检测规则覆盖了C代码质量的多个核心维度我们可以将其归类为以下几个重点领域内存与资源管理这是C的经典难题也是Flint的重中之重。原始指针滥用检测未配对的new/delete、malloc/free强烈建议使用std::unique_ptr或std::shared_ptr。资源获取即初始化RAII检查是否遵循了RAII原则例如文件句柄、锁等资源是否被封装在对象中利用析构函数自动释放。潜在的空指针解引用通过简单的数据流分析标记出那些可能在使用前未被检查的指针解引用操作。面向对象设计良好的面向对象设计是代码可维护性的基石。Rule of Three/Five/Zero自动检查类是否正确地处理了拷贝控制成员拷贝构造、拷贝赋值、移动构造、移动赋值、析构。如果一个类定义了析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个它通常需要全部定义Rule of Three在现代C中可能还需要考虑移动语义Rule of Five或者最好设计成不需要这些特殊成员Rule of Zero。Flint会提示你潜在的违反情况。过深的继承层次与复杂度过高的类过度的继承会导致代码脆弱类过于庞大则违背单一职责原则。Flint可以设置阈值来警告这些情况。虚函数使用检查虚析构函数的使用是否正确以及是否存在隐藏hide而非覆盖override的情况。代码风格与可读性统一的风格能极大提升团队协作效率。命名约定可以配置规则来检查变量、函数、类名是否符合特定的命名规范如驼峰式、蛇形命名法。函数复杂度计算函数的圈复杂度并对过高的复杂度提出警告。圈复杂度高的函数通常难以理解和测试。代码重复检测重复或高度相似的代码块这是代码需要重构提取函数、模板化的强烈信号。性能与现代化推动代码向更现代、更高效的C标准靠拢。不必要的拷贝识别出那些可以通过使用const引用、移动语义或std::string_view来避免的昂贵拷贝操作。C风格代码建议将printf/scanf替换为std::cout/std::cin或格式化库将C风格数组和字符串替换为std::array和std::string。auto关键字的使用可以配置规则来建议或限制auto的使用场景避免过度使用导致类型信息丢失。2.2 设计理念为什么是Flint市面上静态分析工具不少从商业的Coverity、PVS-Studio到开源的Clang-Tidy、Cppcheck。Flint的定位在哪里轻量快速相比于一些需要复杂配置、分析缓慢的“重型”工具Flint的目标是快速反馈。它通常能在一两秒内分析完一个中等规模的文件非常适合集成到编辑器的保存后钩子或CI/CD流水线的预提交检查中实现即时反馈。聚焦“代码异味”它不试图去发现所有可能的运行时错误那是动态分析工具和测试的领域而是专注于那些几乎总是意味着设计有问题或可能引发错误的代码模式。这种聚焦使得它的报告更精准误报率相对较低开发者更愿意采纳其建议。高度可配置不是所有规则都适用于所有项目。Flint允许你通过配置文件如.flintrc来启用、禁用或调整每条规则的严格程度。例如在一个遗留项目中你可能暂时禁用所有关于“使用auto”的警告而在一个全新的、采用C20的项目中你可以启用更严格的现代化规则。教育意义强每条警告信息通常都附带简要的解释和修改建议。对于学习C的开发者来说运行Flint并阅读其输出本身就是一个学习现代C最佳实践的过程。注意Flint与Clang-Tidy并非替代关系而是互补。Clang-Tidy基于强大的Clang AST能进行更深层次、更复杂的分析如更精确的类型推导、模板元编程相关检查。Flint的优势在于其轻量、快速和针对“代码异味”的规则集。在实际项目中可以同时使用两者用Flint做快速日常检查用Clang-Tidy做更全面的定期深度扫描。3. 从零开始Flint的安装与基础配置要让Flint为你工作第一步是把它请到你的开发环境中。由于其开源和跨平台的特性安装过程相当灵活。3.1 安装方式选择1. 从源码编译推荐给追求最新特性或需要定制的用户这是最直接的方式能确保你获得最新版本。# 1. 克隆仓库 git clone https://github.com/L2Program/FlintPlusPlus.git cd FlintPlusPlus # 2. 创建构建目录并进入 mkdir build cd build # 3. 使用CMake生成构建系统 # 如果你没有特定要求使用默认的Makefile即可 cmake .. # 4. 编译 make -j$(nproc) # Linux/macOS, -j参数指定并行编译的线程数加快速度 # 或者在Windows上如果你用MinGW或Visual Studio的开发者命令提示符后续使用 make 或 msbuild # 5. 安装可选将可执行文件安装到系统路径 sudo make install编译完成后在build目录下或src子目录你会找到名为flint的可执行文件。2. 使用包管理器最便捷对于主流Linux发行版和macOS通常可以通过包管理器安装。Ubuntu/Debian:sudo apt-get install flint(注意仓库版本可能较旧)Fedora/RHEL:sudo dnf install flintmacOS (Homebrew):brew install flintArch Linux:sudo pacman -S flint3. 预编译二进制文件部分项目发布页面可能会提供Windows、Linux和macOS的预编译二进制文件直接下载并添加到系统PATH即可。3.2 首次运行与基础配置安装成功后让我们用一个最简单的例子来验证。 创建一个有问题的C文件bad_code.cpp// bad_code.cpp #include iostream class MyClass { int* data; public: MyClass(int size) { data new int[size]; } // 糟糕忘记了析构函数来释放 data }; void riskyFunction(int* ptr) { std::cout *ptr std::endl; // 潜在的空指针解引用 } int main() { MyClass obj(10); int* p nullptr; riskyFunction(p); // 传递空指针 return 0; }在终端运行Flint进行分析flint bad_code.cpp你会看到类似如下的输出bad_code.cpp:5: 警告: Class MyClass has pointer member data but lacks destructor, copy constructor, or copy assignment operator (Rule of Three violation). bad_code.cpp:10: 警告: Potential null pointer dereference: ptr may be null here.看Flint准确地抓住了两个关键问题违反“三法则”和潜在的空指针解引用。基础配置.flintrc文件Flint会在当前目录及其父目录中查找名为.flintrc的配置文件。你可以在这里控制它的行为。 一个基础的.flintrc示例# 启用/禁用特定规则 --checkall # 检查所有规则默认 # --checknone # 不检查任何规则 # --checkmemory,oop # 只检查内存和面向对象相关规则 # 设置圈复杂度阈值超过此值则警告 --cyclomatic-complexity15 # 忽略特定的警告 --suppressrule-of-three # 忽略所有“三法则”警告 # --suppressbad_code.cpp:5 # 忽略文件bad_code.cpp第5行的所有警告 # 输出格式 --formattext # 纯文本格式默认 --formatjson # JSON格式便于其他工具解析 --formatxml # XML格式 # 递归检查目录 --recursive . # 排除某些文件或目录 --exclude./third_party/* --exclude*_test.cpp将这份配置文件放在项目根目录Flint在运行时就会自动应用这些设置。4. 深度集成将Flint融入你的开发工作流单独在命令行运行工具效果有限。真正的威力在于将其无缝集成到你的日常开发环境中实现“左移”的质量保障。4.1 集成到代码编辑器以VS Code为例在VS Code中你可以通过任务Tasks或插件来实现实时或按需检查。方法一使用VS Code任务.vscode/tasks.json这允许你通过快捷键如CtrlShiftB或命令面板运行Flint。{ version: 2.0.0, tasks: [ { label: Run Flint on Current File, type: shell, command: flint, args: [ --formattext, ${file} // 对当前打开的文件运行 ], group: { kind: build, isDefault: true }, presentation: { reveal: always, panel: dedicated, // 在专用输出面板显示结果 clear: true }, problemMatcher: { owner: cpp, fileLocation: [relative, ${workspaceFolder}], pattern: { regexp: ^(.*):(\\d):\\s*(警告|错误):\\s*(.*)$, file: 1, line: 2, severity: 3, message: 4 } } } ] }配置了problemMatcher后Flint的输出会被VS Code识别为“问题”并显示在“问题”面板中你可以直接点击跳转到对应代码行体验和编译器错误一样。方法二使用编辑器插件虽然Flint没有官方的VS Code插件但你可以利用像“Code Runner”或“Shell Command”这类通用插件绑定一个自定义命令来运行Flint。更高级的做法是编写一个简单的VS Code扩展调用Flint并解析其输出。方法三集成到LSP语言服务器协议对于支持LSP的编辑器VS Code, Vim/Neovim with coc.nvim, Emacs with lsp-mode等最理想的方式是让Flint作为一个LSP服务器工作。这需要一些额外的封装工作例如创建一个包装脚本将Flint的输出转换成LSP要求的Diagnostic格式。虽然Flint本身不直接提供LSP服务器但社区可能有相关项目或者你可以自己实现一个简单的版本这能提供最即时的、行内波浪线提示。4.2 集成到版本控制Git Hooks防止“坏代码”进入仓库的最佳防线是预提交钩子pre-commit hook。我们可以在本地提交代码前自动对暂存区staged的C文件运行Flint检查。在项目根目录的.git/hooks/pre-commit文件中添加以下内容如果没有则创建并确保有可执行权限chmod x .git/hooks/pre-commit#!/bin/bash echo Running Flint pre-commit check... # 获取所有暂存的.cpp和.h文件 FILES$(git diff --cached --name-only --diff-filterACM | grep -E \.(cpp|h|hpp|cxx)$) RETVAL0 if [ -n $FILES ]; then # 临时保存Flint输出 OUTPUT$(flint --formattext $FILES 21) if [ $? -ne 0 ] || [ -n $OUTPUT ]; then echo Flint found issues: echo $OUTPUT echo echo Please fix the above issues before committing. # 可以选择是否阻止提交 # RETVAL1 else echo Flint check passed. fi fi exit $RETVAL这个脚本会在你执行git commit时触发。如果Flint发现问题它会打印出来。你可以选择将RETVAL设为1来阻止本次提交强制开发者先修复问题或者只是警告允许临时绕过不推荐长期如此。实操心得在团队中推行代码质量工具时一开始不要设置得太严格。可以先在预提交钩子中只启用少数几个最关键的规则如内存泄漏、空指针并且只警告不阻止提交。等大家习惯后再逐步增加规则并最终设置为阻止提交。这比一开始就设置一堵“高墙”导致所有人抱怨并禁用钩子要有效得多。4.3 集成到CI/CD流水线在持续集成如GitHub Actions, GitLab CI, Jenkins中运行Flint可以确保合并到主分支的代码符合质量标准。这为代码库提供了另一层保护。以下是一个简单的GitHub Actions工作流示例.github/workflows/flint.ymlname: Code Quality Check with Flint on: [push, pull_request] jobs: flint-check: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkoutv3 - name: Install Flint run: sudo apt-get update sudo apt-get install -y flint - name: Run Flint on all C files run: | # 查找所有C源文件和头文件 find . -name *.cpp -o -name *.h -o -name *.hpp -o -name *.cxx | \ grep -v ./build/ | grep -v ./third_party/ | \ xargs flint --formattext --error-exitcode1这个工作流会在每次推送或拉取请求时触发。它安装Flint然后使用find命令定位项目中的所有C文件排除构建目录和第三方库并通过xargs传递给Flint。--error-exitcode1参数使得当Flint发现任何问题时整个步骤会失败从而让CI检查失败阻止合并。这确保了主分支的代码始终通过质量门槛。5. 实战演练使用Flint重构一段典型代码理论说再多不如亲手实践。让我们看一段典型的、存在多种“代码异味”的C代码然后用Flint来诊断并一步步重构它。原始代码 (legacy_code.cpp):#include cstring #include iostream class StringBuffer { char* buffer; int length; public: StringBuffer(const char* str) { length std::strlen(str); buffer new char[length 1]; std::strcpy(buffer, str); } // 缺失拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数 void print() { std::cout buffer std::endl; } // 一个返回内部指针的函数非常危险 char* getBuffer() { return buffer; } }; void processString(StringBuffer sb) { // 按值传递会触发拷贝但类没有定义拷贝构造函数 sb.print(); } int main() { StringBuffer sb1(Hello); StringBuffer sb2 sb1; // 浅拷贝灾难两个对象指向同一块内存。 processString(sb1); // 再次浅拷贝。 // 通过sb2修改了内部缓冲区影响了sb1 std::strcpy(sb2.getBuffer(), Oops); sb1.print(); // 输出变成了 Oops // 程序结束内存被释放两次未定义行为 return 0; }这段代码充满了陷阱原始指针管理、违反“三法则”、不安全的接口、不必要的拷贝。第一步运行Flint诊断flint legacy_code.cpp预期输出会非常“丰富”legacy_code.cpp:4: 警告: Class StringBuffer has pointer member buffer but lacks destructor, copy constructor, or copy assignment operator (Rule of Three violation). legacy_code.cpp:16: 警告: Function getBuffer returns pointer to internal data member. This breaks encapsulation and is dangerous. legacy_code.cpp:20: 警告: Parameter sb of function processString is passed by value. Consider passing by const reference to avoid copy. legacy_code.cpp:20: 注意: Copy constructor of StringBuffer is implicitly deleted because class has user-defined constructor and pointer member. legacy_code.cpp:27: 警告: Potential double free or memory leak due to missing copy control members.Flint一针见血地指出了所有核心问题。第二步基于建议进行重构我们根据Flint的警告应用现代C最佳实践来重构这个类。重构后代码 (modernized_code.cpp):#include iostream #include memory // 为了 std::unique_ptr #include cstring class StringBuffer { std::unique_ptrchar[] buffer; // 使用智能指针自动管理内存 int length; public: // 构造函数 StringBuffer(const char* str) { length std::strlen(str); buffer std::make_uniquechar[](length 1); std::strcpy(buffer.get(), str); } // 规则五由于我们使用了 unique_ptr编译器生成的析构函数、移动构造/赋值已经足够好。 // 我们显式声明默认的移动操作并禁止拷贝因为unique_ptr不可拷贝。 StringBuffer(const StringBuffer) delete; // 禁止拷贝构造 StringBuffer operator(const StringBuffer) delete; // 禁止拷贝赋值 StringBuffer(StringBuffer) default; // 允许移动构造 StringBuffer operator(StringBuffer) default; // 允许移动赋值 ~StringBuffer() default; // 析构函数默认即可 void print() const { // 成员函数不修改对象应声明为const std::cout buffer.get() std::endl; } // 提供安全的访问方式而不是返回原始指针 const char* c_str() const { return buffer.get(); } // 如果需要修改可以提供安全的接口例如一个修改指定位置字符的函数 void setCharAt(size_t index, char c) { if (index length) buffer[index] c; } }; // 参数改为 const 引用避免拷贝 void processString(const StringBuffer sb) { sb.print(); } int main() { StringBuffer sb1(Hello); // StringBuffer sb2 sb1; // 这行现在会编译错误因为拷贝构造被删除了好事 StringBuffer sb2 std::move(sb1); // 我们可以安全地移动 processString(sb2); // 传递常引用高效且安全 // sb2.setCharAt(0, J); // 使用安全的接口修改 // sb2.print(); // 输出 Jello // 不再有双重释放内存由 unique_ptr 自动管理 return 0; }重构要点解析用std::unique_ptrchar[]替代原始指针这是最关键的改变。它自动管理内存的生命周期我们不再需要手动编写析构函数也从根本上防止了浅拷贝因为unique_ptr不可拷贝强制我们思考移动语义。遵循Rule of Five/ZERO我们显式删除了拷贝操作因为深拷贝unique_ptr管理的数组并不简单且可能不是我们想要的并默认了移动操作和析构函数。这明确了类的语义StringBuffer对象是资源的所有者且所有权可以转移但不能共享。提供安全的接口移除了危险的getBuffer()改为提供c_str()只读和setCharAt受控修改等安全接口加强了封装性。使用const正确性将print()和c_str()标记为const明确它们不修改对象状态。将processString的参数改为const StringBuffer避免了不必要的拷贝也更清晰地表达了函数意图。第三步再次运行Flint验证flint modernized_code.cpp如果配置得当这次应该没有任何警告输出或者只有一些与当前项目无关的规则提示。恭喜这段代码的质量已经得到了质的提升。6. 高级技巧与自定义规则当你和团队越来越依赖Flint后你可能会希望它更能贴合自己项目的特殊规范。这时就需要用到它的高级配置和自定义规则能力。6.1 规则微调与过滤Flint的许多规则都有可调节的参数。例如圈复杂度检查--cyclomatic-complexity20这会将警告阈值提高到20。对于某些包含大型switch-case的调度函数适当提高阈值是合理的。你还可以基于目录或文件来应用不同的规则集。例如在项目根目录的.flintrc中# 全局启用所有检查 --checkall --cyclomatic-complexity15 # 但对于 src/legacy/ 目录下的代码我们放宽要求 --config./src/legacy/.flintrc-legacy然后在src/legacy/.flintrc-legacy中# 在遗留代码中只检查最致命的问题 --checkmemory,potential-null-dereference --suppressrule-of-three --cyclomatic-complexity30这种分层配置的策略使得在改造遗留代码库时既能对新代码保持高标准又能对历史代码采取更务实的、渐进式的改进策略。6.2 编写自定义检查规则进阶Flint的真正强大之处在于其可扩展性。虽然官方文档关于自定义规则的细节可能不多但其开源特性意味着你可以修改源码来添加新的检查逻辑。这通常需要你对Flint的代码结构它如何解析C、如何遍历AST有一定的了解。一个典型的自定义规则流程如下定位规则定义在Flint源码的src/rules/目录下你会发现许多.cpp文件每个文件实现了一类或一个检查规则如RuleOfThreeChecker.cpp,NullDereferenceChecker.cpp。理解接口查看这些文件了解规则检查器类是如何继承基类、重写VisitStmt或VisitDecl等函数来访问AST节点的。实现新规则复制一个现有规则文件作为模板修改其核心逻辑。例如你想检查是否有人使用了std::bind而没使用更现代的lambda表达式你可以创建一个PreferLambdaOverBindChecker。注册规则在主要的规则注册文件可能是src/Flint.cpp或某个Registry文件中添加你的新检查器。编译与测试重新编译Flint并用你的代码测试新规则。注意事项自定义规则属于高级用法需要对C AST和Flint内部结构有较深理解。对于大多数团队充分利用内置规则并进行精细配置已经足够。只有当你们有非常特定、且内置规则无法满足的代码规范时才需要考虑自定义。也可以考虑将需求反馈给开源社区或许能成为官方规则的一部分。7. 常见问题与排查技巧实录在实际使用Flint的过程中你可能会遇到一些典型问题。这里记录了一些常见场景和解决思路。7.1 误报与漏报处理问题Flint报告了一个“潜在空指针解引用”但我确信指针在该上下文中不可能为空。排查首先确认你的判断。检查所有可能的分支路径。如果确实无误这属于误报False Positive。解决代码注解有些静态分析工具支持类似// NOLINT或// suppress的注解来忽略下一行或当前行的警告。查看Flint文档是否支持类似机制。配置抑制在.flintrc中使用--suppress选项精确抑制该文件该行的特定警告。例如--suppressmyfile.cpp:123:potential-null-dereference。这是最精准的方式。调整规则如果某个规则在你们的代码库中误报率普遍很高可以考虑在项目级配置中暂时降低该规则的检查级别或直接禁用。问题代码中明显有一个内存泄漏例如new后在某些分支下没有delete但Flint没报出来。排查这属于漏报False Negative。静态分析工具不是万能的尤其是对于路径敏感的数据流分析复杂度很高。解决确认问题使用Valgrind、AddressSanitizer等动态分析工具来确认是否真的存在泄漏。简化代码结构复杂的控制流如深层的嵌套条件、循环和goto会让分析变得困难。考虑重构代码使其逻辑更清晰。使用更强大的工具对于关键模块可以结合使用Clang Static Analyzer、Cppcheck甚至商业工具进行深度扫描。Flint定位是快速检查深度分析需要其他工具补位。反馈给社区如果是一个简单的、典型的泄漏模式Flint却没抓到可以考虑在项目Issue中提交一个案例帮助改进工具。7.2 性能与大规模项目问题在拥有数万个C文件的大型项目上运行Flint非常慢。排查Flint虽然轻量但逐个文件解析和分析大型项目依然耗时。解决增量分析在CI流水线或预提交钩子中只分析本次修改的文件git diff出来的文件而不是整个项目。本章第4.2节的脚本就是例子。并行化利用xargs -P或GNU Parallel等工具并行运行Flint。例如find . -name *.cpp | xargs -P 8 -n 1 flint。注意这可能会使输出交错最好将每个文件的输出重定向到独立日志。缓存机制如果Flint没有内置缓存可以考虑编写脚本记录文件的哈希值如MD5只有当文件内容改变时才重新分析。分模块检查在CI中可以拆分成多个作业每个作业只检查一个子模块。7.3 与编译器的交互问题Flint需要和我的项目使用相同的编译器标志吗比如-stdc17、-I包含路径等。理解Flint是一个独立的源码分析工具它内部有自己的C解析器通常基于类似Clang的库。它不一定需要知道你项目的完整编译标志。影响但是编译标志会影响代码的解析。例如-std标准如果你用了C17的std::optional但Flint以C11模式解析它会不认识这个类型可能导致解析错误或奇怪的警告。-D宏定义如果你的代码通过宏来开启关闭某些特性Flint必须知道这些宏的定义否则会看到完全不同的代码逻辑。-I包含路径Flint需要找到你包含的头文件否则无法进行完整的语法和语义分析。解决方案许多静态分析工具包括Flint的某些版本或分支支持编译数据库Compilation Database通常是一个compile_commands.json文件。这个文件由CMake使用-DCMAKE_EXPORT_COMPILE_COMMANDSON、Bear或compiledb等工具生成记录了每个源文件编译时的完整命令和参数。理想情况如果Flint支持读取compile_commands.json那么它就能以和编译时完全相同的配置来分析代码结果最准确。变通方案如果Flint不支持你可能需要通过其命令行参数手动传递一些关键的宏定义-DXXX和包含路径。对于简单的项目这通常够用。对于复杂项目建议优先寻找支持编译数据库的静态分析工具链。7.4 集成到现代构建系统CMake对于使用CMake的项目你可以创建一个自定义目标让构建系统本身来运行Flint。 在CMakeLists.txt中添加# 查找Flint程序 find_program(FLINTPP_EXECUTABLE flint DOC Path to flint executable) if(FLINTPP_EXECUTABLE) # 添加一个自定义目标 check-code add_custom_target(check-code COMMAND ${FLINTPP_EXECUTABLE} --formattext --checkall ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h COMMENT Running flint code analysis WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) # 你可以让 make check 或 ninja check 依赖于此目标 add_custom_target(check DEPENDS check-code) endif()这样开发者就可以通过运行make check-code或ninja check-code来手动触发代码检查也可以将其作为CI流程中的一步。