尝试把clang-tidy集成到AWTK项目

前言

项目经过一段时间的耕耘终于进入了团队开发阶段,期间出现了很多问题,其中一个就是开会讨论团队的代码风格规范,目前项目代码风格比较混乱,有的模块是驼峰,有的模块是匈牙利,后面经过讨论,决定采用匈牙利,和awtk库的api风格一致。

讨论完之后就是改代码了,有十几个模块几百个函数要改,一个个人工去改显然费时费力,改的时候就在想这种东西有没有自动化的做法了?

于是下班开始探索一番,首先是尝试用AI写一个批量扫描文件,用正则匹配不符合规则的python脚本,结果费时费力,效果明显不好。

不对,c/c++发展了十几年,这类问题难道没有现成的方案?

后面搜了点原理,正则的思路对于这类问题显然不对,没法识别函数和变量,那只能用抽象语法树了。对象是c/c++,应该是一个类似c/c++编译器的静态分析工具。

最后找到了clang-tidy。

尝试集成1.0

首先下载clang-tidy:

pip install clang-tidy

假设示例项目src结构如下:

src
├── application.c
├── common
│   ├── navigator.c
│   ├── navigator.h
│   └── temperature.h
├── main.c
├── modules
│   ├── libframebuffer.c
│   └── libframebuffer.h
├── pages
│   └── home_page.c
└── SConscript

要修改的文件例:libframebuffer,里面api是驼峰格式,需要自动改成匈牙利格式,home_page有引用。

libframebuffer.h

#ifndef LIBFRAMEBUFFER_H
#define LIBFRAMEBUFFER_H
typedef struct {
    int width;
    int height;
    int bpp;
    void* buffer;
} LibFrameBuffer;

LibFrameBuffer* LibFrameBufferInit();

void LibFrameBufferFree(LibFrameBuffer* fb);

#endif 	

libframebuffer.c

#include "libframebuffer.h"
#include <stdlib.h>

LibFrameBuffer* LibFrameBufferInit()
{
    return (LibFrameBuffer*)malloc(sizeof(LibFrameBuffer));
}

void LibFrameBufferFree(LibFrameBuffer* fb)
{
    free(fb);
}

home_page.c

#include "awtk.h"
#include "libframebuffer.h"

/**
 * 初始化窗口的子控件
 */
static ret_t visit_init_child(void* ctx, const void* iter) {
  widget_t* win = WIDGET(ctx);
  widget_t* widget = WIDGET(iter);
  const char* name = widget->name;

  // 初始化指定名称的控件(设置属性或注册事件),请保证控件名称在窗口上唯一
  if (name != NULL && *name != '\0') {

  }

  return RET_OK;
}

/**
 * 初始化窗口
 */
ret_t home_page_init(widget_t* win, void* ctx) {
  (void)ctx;
  return_value_if_fail(win != NULL, RET_BAD_PARAMS);

  widget_foreach(win, visit_init_child, win);
  LibFrameBufferInit();
  return RET_OK;
}

在项目根目录下设置好.clang-tidy,这个是clang-tidy的配置文件, clang-tidy有很多的check项,和代码命名风格相关的是readability-identifier-naming,这个checker下面有非常多类型的拼写设置,我这里设置了类, 变量,函数,宏四个类型的拼写项,其中前三个的lower_case对应的就是匈牙利小写写法,最后一个UPPER_CASE是全大写写法。

readability-identifier-naming的具体设置可见:https://clang.llvm.org/extra/clang-tidy/checks/readability/identifier-naming.html

Checks: >
  readability-identifier-naming
CheckOptions:
  - key:             readability-identifier-naming.ClassCase
    value:           lower_case
  - key:             readability-identifier-naming.VariableCase
    value:           lower_case
  - key:             readability-identifier-naming.FunctionCase
    value:           lower_case
  - key:             readability-identifier-naming.MacroDefinitionCase
    value:           UPPER_CASE

clang-tidy本身相当于半个编译器,会对翻译单元的头文件进行处理,而不是像脚本那样单纯进行文本解析,所以在给clang-tidy一个找不到头文件的源文件时会导致报错:

zhangdalin@huwyi-ubuntu:~/AWStudioProjects/awtk_clang_tidy_test$ clang-tidy src/pages/home_page.
Error while trying to load a compilation database:
Could not auto-detect compilation database for file "src/pages/home_page."
No compilation database found in /home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/pages or any parent directory
fixed-compilation-database: Error while opening fixed database: No such file or directory
json-compilation-database: Error while opening JSON database: No such file or directory
Running without flags.
Error while processing /home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/pages/home_page..
error: no input files [clang-diagnostic-error]
error: no such file or directory: '/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/pages/home_page.' [clang-diagnostic-error]
error: unable to handle compilation, expected exactly one compiler job in '' [clang-diagnostic-error]
Found compiler error(s).

因此必须找到一个方法让clang-tidy能够读取到项目所有的源文件和头文件。

使用compiler_command.json

compiler_command.json上面记录了项目每个编译的编译命令,输入源文件和输出obj,类似于给工具一个符号表,clang-tidy通过这张符号表进行解析。

在CMake可以设置set(CMAKE_EXPORT_COMPILE_COMMANDS ON)生成compiler_command.json给clang-tidy,我于是去看scons有没有对应设置,还好有,见:https://scons.org/doc/latest/HTML/scons-user/ch27.html

在SConstruct和SConscript分别添加env.Tool('compilation_db')env.CompilationDatabase()[0], scons就能在BIN_DIR得到compiler_command.json,我试过在SConscript把两个指令一起加进去,不知道为什么在windows会报找不到AttributeError: 'SConsEnvironment' object has no attribute '__COMPILATIONDB_Entry':的错误,Linux上就正常。我这里的情形是跨平台,windows和linux都需要考虑使用。

SConstruct

+env = helper.call(DefaultEnvironment)
+env.Tool('compilation_db')

SConscript

sources = Glob('**/*.c') + Glob('*.c')
+compile_database = env.CompilationDatabase()[0]

scons编译,在BIN_DIR得到compiler_command.json,验证通过。

接下来写clang-tidy调用逻辑,scons本身是python脚本,直接封装成函数就好了,难点在于如何让scons执行时,如果需要将封装函数也一并调用,对此,scons提供了Options选项, 可以自定义选项来控制调用逻辑,见:https://scons.org/doc/latest/HTML/scons-user/ch10.html

这里就提供一个--clang-tidy选项给scons,修改后的SConscript:

import os
import subprocess
from SCons.Script import *

env = DefaultEnvironment().Clone()
BIN_DIR = os.environ['BIN_DIR']
LIB_DIR = os.environ['LIB_DIR']

sources = Glob('**/*.c') + Glob('*.c')

AddOption('--clang-tidy',
                dest='clang_tidy',
                metavar='BOOL',
                action='store_true',
                default=False,
                help="run clang-tidy static code analysis")
compile_database = env.CompilationDatabase()[0]
program = env.Program(os.path.join(BIN_DIR, 'demo'), sources, LIBS = env['LIBS'])

def run_clang_tidy(target, source, env):
    source_file = str(source[0])
    compilation_db = str(source[1])

    cmd = f"clang-tidy -p {compilation_db} -header-filter=.* {source_file}"
    print(f"Running: {cmd}")
    subprocess.run(cmd, shell=True)
    return 0

print(f"clang_tidy option = {GetOption('clang_tidy')}")

if GetOption('clang_tidy') == True:
    for source in sources:
        run_clang_tidy('',[source, compile_database], env)

执行,会看到clang-tidy把三方库的头文件也加进了检测,原因是设置用了-header-filter=.*来将项目头文件加入检测,不加这个选项是只能检测c文件的,而且函数只能检测到没有加入头文件声明的函数。

/home/zhangdalin/AWStudioProjects/awtk/3rd/SDL/include/SDL_stdinc.h:569:9: warning: invalid case style for macro definition 'SDL_malloc' [readability-identifier-naming]
#define SDL_malloc malloc
        ^~~~~~~~~~
        SDL_MALLOC
/home/zhangdalin/AWStudioProjects/awtk/3rd/SDL/include/SDL_stdinc.h:570:9: warning: invalid case style for macro definition 'SDL_calloc' [readability-identifier-naming]
#define SDL_calloc calloc
        ^~~~~~~~~~
        SDL_CALLOC
/home/zhangdalin/AWStudioProjects/awtk/3rd/SDL/include/SDL_stdinc.h:571:9: warning: invalid case style for macro definition 'SDL_realloc' [readability-identifier-naming]
#define SDL_realloc realloc
        ^~~~~~~~~~~
        SDL_REALLOC

解决方法是在.clang-tidy下设置HeaderFilterRegex, 原理是clang-tidy搜索的文件是绝对路径的列表,设置HeaderFilterRegex将只检测和设置的正则匹配的文件路径。

注意原来脚本的-header-filter=.*要去掉,否则会覆盖.clang-tidy上的设置,那就失效了。

.clang-tidy

HeaderFilterRegex: 'awtk_clang_tidy_test\\src\\.*|awtk_clang_tidy_test/src/.*' #左边匹配windows下项目路径,右边匹配linux下项目路径

SConscript

def run_clang_tidy(target, source, env):
    source_file = str(source[0])
    compilation_db = str(source[1])

    cmd = f"clang-tidy -p {compilation_db} {source_file}"
    print(f"Running: {cmd}")
    subprocess.run(cmd, shell=True)
    return 0

自此一个基础的集成就完成了。

Running: clang-tidy -p compile_commands.json common/navigator.c
2024 warnings generated.
Suppressed 2024 warnings (2024 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
Running: clang-tidy -p compile_commands.json modules/libframebuffer.c
679 warnings generated.
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/modules/libframebuffer.c:4:17: warning: invalid case style for function 'LibFrameBufferInit' [readability-identifier-naming]
LibFrameBuffer* LibFrameBufferInit()
                ^~~~~~~~~~~~~~~~~~
                lib_frame_buffer_init
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/modules/libframebuffer.c:9:6: warning: invalid case style for function 'LibFrameBufferFree' [readability-identifier-naming]
void LibFrameBufferFree(LibFrameBuffer* fb)
     ^~~~~~~~~~~~~~~~~~
     lib_frame_buffer_free

尝试集成2.0

看似简单,实际引用到项目还是有问题,scons不像CMake, scons编译compiler_command.json的过程和编译项目的过程是绑死的,一定要编译完项目才会生成compiler_command.json,我曾经试过把env.Program只生成json,发现json能够出来,但是里面不带任何项目相关的文件编译命令,只有编译整个项目才能更新compiler_command.json

这就是scons的不便之处了,项目是没法先静态检测再编译的,只能放到编译后再检测了。如果是CMake,大可以通过cmake -S. -B buildcmake --build build把构建compiler_command.json和编译项目的过程按先后分开。

怎么让scons在env.Program后执行自定义命令?可以用env.Command, scons编译先后顺序涉及到builder的概念,这里就不深入了,参考:

https://stackoverflow.com/questions/36273482/scons-strange-execution-order

https://scons.org/doc/1.3.0/HTML/scons-user/c3721.html

修改后的SConscript如下:

import os
import subprocess
from SCons.Script import *

env = DefaultEnvironment().Clone()
BIN_DIR = os.environ['BIN_DIR']
LIB_DIR = os.environ['LIB_DIR']

sources = Glob('**/*.c') + Glob('*.c')

AddOption('--clang-tidy',
                dest='clang_tidy',
                metavar='BOOL',
                action='store_true',
                default=False,
                help="run clang-tidy static code analysis")
compile_database = env.CompilationDatabase()[0]
program = env.Program(os.path.join(BIN_DIR, 'demo'), sources, LIBS = env['LIBS'])

def run_clang_tidy(target, source, env):
    source_file = str(source[0])
    compilation_db = str(source[1])

    cmd = f"clang-tidy -p {compilation_db}  {source_file}"
    print(f"Running: {cmd}")
    subprocess.run(cmd, shell=True)
    return 0

print(f"clang_tidy option = {GetOption('clang_tidy')}")

if GetOption('clang_tidy') == True:
    for source in sources:
        # 会在env.Program之后执行
        env.Command(
            target=f"{os.path.basename(str(source))}.clang-tidy.log",
            source=[source, compile_database],
            action=Action(run_clang_tidy, cmdstr="Running clang-tidy on ${SOURCE}")
        )

把编译出的compile_commands.json删了,然后scons --clang-tidy测试,正常运行:

zhangdalin@huwyi-ubuntu:~/AWStudioProjects/awtk_clang_tidy_test$ scons --clang-tidy
scons: Reading SConscript files ...
APP_SCRIPTS_ROOT:/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/scripts
AWTK_ROOT: /home/zhangdalin/AWStudioProjects/awtk
AWTK_SCRIPTS_ROOT: /home/zhangdalin/AWStudioProjects/awtk/scripts
...
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/bin exist.
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/lib exist.
False
AWTK_ROOT: /home/zhangdalin/AWStudioProjects/awtk
TKC_ONLY: False
{}
/home/zhangdalin/AWStudioProjects/awtk/bin/libawtk.so==>/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/bin
clang_tidy option = True
scons: done reading SConscript files.
scons: Building targets ...
Building compilation database src/compile_commands.json
Running clang-tidy on src/application.c
Running: clang-tidy -p src/compile_commands.json  src/application.c
2024 warnings generated.
Suppressed 2024 warnings (2024 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
Running clang-tidy on src/pages/home_page.c
Running: clang-tidy -p src/compile_commands.json  src/pages/home_page.c
2027 warnings generated.
src/pages/home_page.c:8:13: warning: unused variable 'win' [clang-diagnostic-unused-variable]
  widget_t* win = WIDGET(ctx);
            ^
Suppressed 2026 warnings (2026 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
Running clang-tidy on src/modules/libframebuffer.c
Running: clang-tidy -p src/compile_commands.json  src/modules/libframebuffer.c
679 warnings generated.
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/modules/libframebuffer.c:4:17: warning: invalid case style for function 'LibFrameBufferInit' [readability-identifier-naming]
LibFrameBuffer* LibFrameBufferInit()
                ^~~~~~~~~~~~~~~~~~
                lib_frame_buffer_init
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/modules/libframebuffer.c:9:6: warning: invalid case style for function 'LibFrameBufferFree' [readability-identifier-naming]
void LibFrameBufferFree(LibFrameBuffer* fb)
     ^~~~~~~~~~~~~~~~~~
     lib_frame_buffer_free
Suppressed 677 warnings (677 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
Running clang-tidy on src/main.c
Running: clang-tidy -p src/compile_commands.json  src/main.c
2028 warnings generated.
Suppressed 2028 warnings (2028 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
Running clang-tidy on src/common/navigator.c
Running: clang-tidy -p src/compile_commands.json  src/common/navigator.c
2024 warnings generated.
Suppressed 2024 warnings (2024 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
scons: done building targets.

能正常运行了,这时候就可以加--fix选项去自动改naming错误了,我具体测试了下,还是没法全部一次性修改的,有些引用函数的文件就没有改到,在这个例子是只改了libframebuffer.c,libframebuffer.h没有修改。好在数量少,自动化工具能解决大部分。

然而还是有问题,这个方案只能在linux下使用,在windows上会报unable to handle compilation错误:

Running clang-tidy on src\modules\libframebuffer.c
Running: clang-tidy --fix -p src\compile_commands.json  src\modules\libframebuffer.c
Error while processing D:\AWStudioProjects\awtk_clang_tidy_test\src\modules\libframebuffer.c.
error: unable to handle compilation, expected exactly one compiler job in '' [clang-diagnostic-error]
warning: @C:\Users\z5843\AppData\Local\Temp\tmp_8x5etvp.lnk: 'linker' input unused [clang-diagnostic-unused-command-line-argument]
Found compiler errors, but -fix-errors was not specified.
Fixes have NOT been applied.

原因是我awtk在windows上设置的用msvc编译,scons对于msvc生成的compiler_command.json里面调用指令实际上要通过一层快捷方式文件去中转,称之为响应文件(response files), 因为windows的命令行有长度限制,没法一次调用太长的命令。

[
    {
        "command": "cl @C:\\Users\\z5843\\AppData\\Local\\Temp\\tmpwbrtpkke.lnk",
        "directory": "D:\\AWStudioProjects\\awtk_clang_tidy_test",
        "file": "src\\common\\navigator.c",
        "output": "src\\common\\navigator.obj"
    },
    {
        "command": "cl @C:\\Users\\z5843\\AppData\\Local\\Temp\\tmpmf4y8gp4.lnk",
        "directory": "D:\\AWStudioProjects\\awtk_clang_tidy_test",
        "file": "src\\modules\\libframebuffer.c",
        "output": "src\\modules\\libframebuffer.obj"
    },
    {
        "command": "cl @C:\\Users\\z5843\\AppData\\Local\\Temp\\tmpjye3ldpl.lnk",
        "directory": "D:\\AWStudioProjects\\awtk_clang_tidy_test",
        "file": "src\\pages\\home_page.c",
        "output": "src\\pages\\home_page.obj"
    },
    {
        "command": "cl @C:\\Users\\z5843\\AppData\\Local\\Temp\\tmpjtyr5i36.lnk",
        "directory": "D:\\AWStudioProjects\\awtk_clang_tidy_test",
        "file": "src\\application.c",
        "output": "src\\application.obj"
    },
    {
        "command": "cl @C:\\Users\\z5843\\AppData\\Local\\Temp\\tmpn639b_bf.lnk",
        "directory": "D:\\AWStudioProjects\\awtk_clang_tidy_test",
        "file": "src\\main.c",
        "output": "src\\main.obj"
    },
    {
        "command": "cl @C:\\Users\\z5843\\AppData\\Local\\Temp\\tmpghyoi9f_.lnk",
        "directory": "D:\\AWStudioProjects\\awtk_clang_tidy_test",
        "file": "D:\\AWStudioProjects\\awtk\\3rd\\gtest\\googletest\\src\\gtest-all.cc",
        "output": "D:\\AWStudioProjects\\awtk\\3rd\\gtest\\googletest\\src\\gtest-all.obj"
    },
    {
        "command": "cl @C:\\Users\\z5843\\AppData\\Local\\Temp\\tmpubp7ar8o.lnk",
        "directory": "D:\\AWStudioProjects\\awtk_clang_tidy_test",
        "file": "tests\\main.cc",
        "output": "tests\\main.obj"
    }
]

这些快捷方式第一次编译才会创建,在第二次编译时就能复用,而且如果修改了源文件(比如修改代码或者开关宏),这些快捷方式就失效了,这就是为什么我改源文件或者删除compiler_command.json都会导致报unable to handle compilation错误。我在网上找了一圈都不知道怎么关掉这个响应文件。

其实也可以设置awtk编译方式为MINGW, 这样就没有问题了,可惜我实际项目一些三方库是msvc编译的,和mingw不兼容,这个选择就废了。

没法泛化到windows,方案还要继续改进。

BTW: CMake+MSVC在windows上是不会生成compiler_command.json的,不知道scons怎么做到的生成。

msvc就你离谱

尝试集成3.0

其实不使用compiler_command.json, 也可以通过给clang-tidy直接指明源文件和头文件路径的方式来绕过,这样检测就不依赖于编译了。

可以通过直接用scons的配置文件,比较方便,也可以独立脚本,不过路径需要脚本去暴力匹配,如果项目庞大,写起匹配逻辑会比较麻烦,这里选用前者。

之前的脚本其实还有个问题,run_clang_tidy执行一次只检测一个文件,一个文件就执行一次clang-tidy命令,这样使用是低效的,实际上clang-tidy可以批量集成多个文件一起去分析。

run_clang_tidy修改,拆除来放到clang_tidy_helper.py,置于scripts文件夹:

import subprocess

def run_clang_tidy(flags, source_file_str, cxxflags, include_file_str):
    cmd = f"clang-tidy {flags} {source_file_str} -- {cxxflags} {include_file_str}"
    print(f"Running: {cmd}")
    subprocess.run(cmd, shell=True)
    return 0
    

修改后的SConscript如下:

import os
import subprocess
from scripts.clang_tidy_helper import run_clang_tidy
from SCons.Script import *

env = DefaultEnvironment().Clone()
BIN_DIR = os.environ['BIN_DIR']
LIB_DIR = os.environ['LIB_DIR']

sources = Glob('**/*.c') + Glob('*.c')

AddOption('--clang-tidy',
                dest='clang_tidy',
                metavar='BOOL',
                action='store_true',
                default=False,
                help="run clang-tidy static code analysis")


program = env.Program(os.path.join(BIN_DIR, 'demo'), sources, LIBS = env['LIBS'])


if GetOption('clang_tidy') == True:
    sources_file_str = ' '.join(os.path.normpath(str(s)) for s in sources)    
    flags = env['CCFLAGS'] 
    include_file_str = ' '.join([f'-I{os.path.normpath(str(i))}' for i in env['CPPPATH']])
    run_clang_tidy('', sources_file_str, flags, include_file_str)

运行,可以看到输出不一样了,会统计文件数,累计warning和error数,能检测出libframebuffer的naming错误,不过这次头文件和源文件的error都可以显示了,加入–fix选项运行,发现头文件,源文件,引用的文件都被修正了。

/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/modules/libframebuffer.c:4:17: warning: invalid case style for function 'LibFrameBufferInit' [readability-identifier-naming]
LibFrameBuffer* LibFrameBufferInit()
                ^~~~~~~~~~~~~~~~~~
                lib_frame_buffer_init
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/modules/libframebuffer.c:4:17: note: FIX-IT applied suggested code changes
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/modules/libframebuffer.c:9:6: warning: invalid case style for function 'LibFrameBufferFree' [readability-identifier-naming]
void LibFrameBufferFree(LibFrameBuffer* fb)
     ^~~~~~~~~~~~~~~~~~
     lib_frame_buffer_free
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/modules/libframebuffer.c:9:6: note: FIX-IT applied suggested code changes
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/modules/libframebuffer.h:10:17: warning: invalid case style for function 'LibFrameBufferInit' [readability-identifier-naming]
LibFrameBuffer* LibFrameBufferInit();
                ^~~~~~~~~~~~~~~~~~
                lib_frame_buffer_init
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/modules/libframebuffer.h:10:17: note: FIX-IT applied suggested code changes
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/modules/libframebuffer.h:12:6: warning: invalid case style for function 'LibFrameBufferFree' [readability-identifier-naming]
void LibFrameBufferFree(LibFrameBuffer* fb);
     ^~~~~~~~~~~~~~~~~~
     lib_frame_buffer_free
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/modules/libframebuffer.h:12:6: note: FIX-IT applied suggested code changes
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/pages/../modules/libframebuffer.h:10:17: warning: invalid case style for function 'LibFrameBufferInit' [readability-identifier-naming]
LibFrameBuffer* LibFrameBufferInit();
                ^~~~~~~~~~~~~~~~~~
                lib_frame_buffer_init
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/pages/../modules/libframebuffer.h:10:17: note: FIX-IT applied suggested code changes
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/pages/home_page.c:28:3: note: FIX-IT applied suggested code changes
  LibFrameBufferInit();
  ^
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/pages/../modules/libframebuffer.h:12:6: warning: invalid case style for function 'LibFrameBufferFree' [readability-identifier-naming]
void LibFrameBufferFree(LibFrameBuffer* fb);
     ^~~~~~~~~~~~~~~~~~
     lib_frame_buffer_free
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/pages/../modules/libframebuffer.h:12:6: note: FIX-IT applied suggested code changes
/home/zhangdalin/AWStudioProjects/awtk_clang_tidy_test/src/pages/home_page.c:8:13: warning: unused variable 'win' [clang-diagnostic-unused-variable]
  widget_t* win = WIDGET(ctx);
            ^
clang-tidy applied 7 of 7 suggested fixes.
Suppressed 8802 warnings (8802 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
scons: done reading SConscript files.
scons: Building targets ...
scons: `.' is up to date.
scons: done building targets.

如果不想编译项目,单独做静态检测,也比较简单:

AddOption('--no-compile',
                dest='no_compile',
                metavar='BOOL',
                action='store_true',
                default=False,
                help="Don't compile the code")

if GetOption('no_compile') == False:
    program = env.Program(os.path.join(BIN_DIR, 'demo'), sources, LIBS = env['LIBS'])

从这边开始,总算就没有什么比较难受的使用问题了,而且可以集成检测工具一起编译,也可以单独检测。

还有很多细节,比如改用run-clang-tidy.py脚本提速,ci/cd集成,时间原因先写到这里,后面有时间看看如何实现。

最终的脚本:

SConscript

import os
import subprocess
from scripts.clang_tidy_helper import run_clang_tidy
from SCons.Script import *

env = DefaultEnvironment().Clone()
BIN_DIR = os.environ['BIN_DIR']
LIB_DIR = os.environ['LIB_DIR']

sources = Glob('**/*.c') + Glob('*.c')

AddOption('--clang-tidy',
                dest='clang_tidy',
                metavar='BOOL',
                action='store_true',
                default=False,
                help="run clang-tidy static code analysis")



AddOption('--no-compile',
                dest='no_compile',
                metavar='BOOL',
                action='store_true',
                default=False,
                help="Don't compile the code")

if GetOption('no_compile') == False:
    program = env.Program(os.path.join(BIN_DIR, 'demo'), sources, LIBS = env['LIBS'])


if GetOption('clang_tidy') == True:
    sources_file_str = ' '.join(os.path.normpath(str(s.get_abspath())) for s in sources)    
    flags = env['CCFLAGS'] 
    include_file_str = ' '.join([f'-I{os.path.normpath(str(i))}' for i in env['CPPPATH']])
    run_clang_tidy('--fix ', sources_file_str, flags, include_file_str)

SConstruct

import os
import scripts.app_helper as app
import clang_tidy as tidy

CUSTOM_WIDGET_LIBS = [{
    "root" : '3rd/awtk-widget-label-rotate',
    'shared_libs': ['label_rotate'],
    'static_libs': []
}]

DEPENDS_LIBS = CUSTOM_WIDGET_LIBS + []

helper = app.Helper(ARGUMENTS)
helper.set_deps(DEPENDS_LIBS)

# app.prepare_depends_libs(ARGUMENTS, helper, DEPENDS_LIBS)
env = helper.call(DefaultEnvironment)

SConscriptFiles = ['src/SConscript', 'tests/SConscript']
helper.SConscript(SConscriptFiles)

scripts/clang_tidy_helper.py

import os
import sys
import platform
import re
import subprocess

def run_clang_tidy(flags, source_file_str, cxxflags, include_file_str):
    cmd = f"clang-tidy {flags} {source_file_str} -- {cxxflags} {include_file_str}"
    print(f"Running: {cmd}")
    subprocess.run(cmd, shell=True)
    return 0
    

补充

clang-tidy不一定能一次解决所有的代码规范问题,比如私有函数习惯写__前缀,即使是用匈牙利写法也会被误报:

D:\AWStudioProjects\awtk_clang_tidy_test\src\modules\lib_framebuffer.c:11:6: warning: invalid case style for function '__lib_framebuffer_load_format' [readability-identifier-naming]      
   11 | void __lib_framebuffer_load_format(lib_framebuffer *fb){
      |      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      |      lib_framebuffer_load_format
D:\AWStudioProjects\awtk_clang_tidy_test\src\modules\lib_framebuffer.c:31:6: warning: invalid case style for function '__lib_framebuffer_init' [readability-identifier-naming]
   31 | void __lib_framebuffer_init(lib_framebuffer *fb, const char *path) {
      |      ^~~~~~~~~~~~~~~~~~~~~~
      |      lib_framebuffer_init
D:\AWStudioProjects\awtk_clang_tidy_test\src\modules\lib_framebuffer.c:73:6: warning: invalid case style for function '__lib_framebuffer_destroy' [readability-identifier-naming]
   73 | void __lib_framebuffer_destroy(lib_framebuffer *fb) {
      |      ^~~~~~~~~~~~~~~~~~~~~~~~~
      |      lib_framebuffer_destroy

clang-tidy做了一个workaround, 允许用正则的方式去忽略匹配的命名样式。

注意必须是全字匹配,部分匹配是不起效的。

Checks: >
  readability-identifier-naming
CheckOptions:
  - key:             readability-identifier-naming.ClassCase
    value:           lower_case
  - key:             readability-identifier-naming.VariableCase
    value:           lower_case
  - key:             readability-identifier-naming.FunctionIgnoredRegexp
    value:           '__.*'
  - key:             readability-identifier-naming.ConstexprVariableCase
    value:           UPPER_CASE
 HeaderFilterRegex: 'awtk_clang_tidy_test\\src\\.*|awtk_clang_tidy_test/src/.*' #左边匹配windows下项目路径,右边匹配linux下项目路径

参考

https://github.com/SCons/scons/issues/4637

https://scons.org/doc/latest/HTML/scons-user/ch27.html

https://lrita.github.io/2023/03/21/auto-clang-tidy-cpp-code/

<think>好的,用户想了解如何将clang-tidy集成项目中。我需要分步骤解释,确保结构清晰,同时遵循用户的格式要求。首先,用户可能对clang-tidy不太熟悉,我应该先简要介绍它是什么,有什么作用。 接下来,用户可能需要具体的步骤。常见的集成方法包括命令行手动运行、构建系统集成(比如CMake)、持续集成(CI)以及编辑器/IDE集成。这些都需要分别说明,每个步骤要详细但不过于复杂。 需要注意的是,用户可能使用不同的构建系统,但CMake是比较常见的,所以重点讲CMake的方法。同时,要提到配置文件的使用,比如创建.clang-tidy文件,这样用户可以根据项目需要自定义检查规则。 另外,用户可能对如何自定义检查规则感兴趣,所以需要解释如何修改.clang-tidy文件,选择需要的检查项,排除不需要的。还要提到禁用某些检查的方法,比如NOLINT注释。 可能遇到的问题,比如误报或编译数据库生成,也需要给出解决方案。例如,使用bear或scan-build来生成compile_commands.json,特别是当项目不是用CMake构建时。 最后,补充一些实际建议,比如逐步启用检查,结合CI,排除第三方代码等,帮助用户更好地落地集成clang-tidy。需要确保语言口语化,避免使用专业术语过多,但又要准确。同时,检查是否所有数学公式都正确使用了$...$或$$...$$格式,但在这个问题中可能涉及较少,不过要留意用户提到的系统级指令,确保没有遗漏。</think>我们将分步骤介绍将clang-tidy集成项目中的方法(使用$$...$$格式表示独立命令行操作): 一、基础准备 1. 安装clang-tidy $$ sudo apt-get install clang-tidy$$ # Ubuntu/Debian $$ brew install llvm$$ # macOS 2. 生成编译数据库 对于CMake项目: $$ mkdir build && cd build $$ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .. $$ mv compile_commands.json ../$$ 二、核心集成方法 1. 命令行直接运行 基本格式: $$ clang-tidy -p build_dir source_file.cpp --header-filter=.*$$ 2. CMake集成(推荐) 在CMakeLists.txt中添加: ```cmake set(CMAKE_CXX_CLANG_TIDY "clang-tidy;-checks=*;-header-filter=.*") ``` 3. 创建配置文件 新建.clang-tidy文件示例: ```yaml Checks: > -*, clang-*, modernize-*, performance-*, readability-* WarningsAsErrors: '' HeaderFilterRegex: '.*' ``` 三、进阶配置技巧 1. 指定检查规则 $$ clang-tidy -checks="modernize-*,readability-*" ...$$ 2. 排除特定目录 ```yaml HeaderFilterRegex: '^(src|include)/' ``` 3. 与CI/CD集成 GitLab CI示例: ```yaml lint: script: - clang-tidy --version - run-clang-tidy -p ${CI_PROJECT_DIR}/build ``` 四、典型问题解决方案 1. 处理第三方库 通过编译选项排除: $$ -isystem /path/to/third_party$$ 2. 禁用特定检查 在代码中添加: ```cpp // NOLINTNEXTLINE(check-name) void deprecated_function() {} ``` 五、最佳实践建议 1. 分阶段启用检查: - 第一阶段:启用基本检查(readability-*) - 第二阶段:添加modernize-* - 第三阶段:启用performance-* 2. 建议配合工具链: ```mermaid graph LR A[代码提交] --> B{触发CI} B -->|失败| C[本地运行clang-tidy] C --> D[修复问题] D --> A ``` 注意:实际使用时应根据项目特点调整检查规则,建议先通过$$ clang-tidy --list-checks$$查看所有可用检查项,逐步优化配置。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值