vscode中使用clang-tidy

介绍

clang-tidy是一个开源的lint工具。
它的主要作用:
a) 自动化检查代码格式是否满足要求
b) 增强编译器的检查功能,提示可能出错或有性能问题的代码

背靠clang/llvm的强大能力,clang-tidy提供了极强的定制和扩展能力。
这使得很多新的大型C/C++项目从项目初始就启用clang-tidy。

vscode上使用clang-tidy

在vscode上使用clang-tidy很简单,只需要安装Clang-Tidy插件就可以了。
该插件的基本原理是调用clang-tidy –export-fixes=- 输出文本,然后解析文本后组装为vs能识别的告警信息。

准备环境

安装clang-tidy并配置好插件

首先需要安装clang-tidy,使用apt安装或者自行编译都可以。
然后安装Clang-Tidy插件,并确保插件配置能找到clang-tidy的程序(确保路径或者PATH正确)。

为工程中的代码生成compile_commands.json

clang-tidy和许多clang体系工具一样,知道源代码编译命令后可以工作得更好。
由于源代码文件众多,实际上可操作的方法只有使用编译系统自动生成的编译命令记录compile_commands.json。使用cmake的体系,添加-DCMAKE_EXPORT_COMPILE_COMMANDS=ON就能自动生成该文件。其他构建体系也有类似的解决方案,可参考https://sarcasm.github.io/notes/dev/compilation-database.html
生成该文件后,还需要注意把这个.json放置到源代码的父目录下,否则clang-tidy会找不到。如果在${top_dir}/build中构建工程并生成了compile_commands.json,但是代码在${top_dir}/src中,则clang-tidy无法自动找到compile_commands.json,需要把其拷贝到${top_dir}下。

修复Clang-Tidy不支持中文的bug

Clang-Tidy使用了clang-tidy文本输出YAML格式的部分(来自 –export-fixes部分)。
示例如下:

1
2
3
4
5
6
7
8
MainSourceFile:  '/media/majiang/c6b38ac3-8b8a-4613-8259-dddbffe2f4cb/majiang/cpp_exercise/llvm_study_Kaleidoscope/./main.cpp'
Diagnostics:
- DiagnosticName: cppcoreguidelines-pro-type-vararg
DiagnosticMessage:
Message: do not call c-style vararg functions
FilePath: '/tmp/test_main.cpp'
FileOffset: 1266
Replacements: []

最核心的信息是FilePath和FileOffset,这两个信息给出了Vscode界面应该在哪里显示告警。
但不幸的是,FileOffset这个值是clang-tidy给其自动修复工具用的,所以其值是一个以byte计数的偏移。
而在vscode中,文件位置的offset不是以byte记的,而是以字符来计算的。如果混入了中文等多byte字符,则vscode中的offset数值将小于clang-tidy给出的FileOffset。

更加糟糕的是,vscode当前没有给出把一个FileOffset转换为行号和列号的接口。其只提供了TextDocument.positionAt(offset: number)。这里的offset是以字符记的。看起来vscode是把单个字符当做了最小单元(哪怕这个字符实际上对应多个byte,可能这样对上层抽象的处理更加容易)。
由于上面描述的问题,一旦代码中出现中文等多byte字符,Clang-Tidy插件给出的告警就会向下漂移(由于其调用了TextDocument.positionAt,并且传入的是以byte记的offset,所以计算出的lineno要更大)。

参考 https://github.com/notskm/vscode-clang-tidy/issues/13,已经有人提到了这个问题,并且作者也给出了与我同样的分析,但是没有提出解决方案。

但是,实际上clang-tidy在非YAML部分其实已经给出了正确的行号和列号,如下所示。

1
main.cpp:46:3: warning: do not call c-style vararg functions [cppcoreguidelines-pro-type-vararg]

很奇怪的是Clang-Tidy插件专门从这一行中提取了warning这个关键字用来计算提示信息的严重程度,但是没用这里的行号和列号。
一种快速的规避方案,可以就从这里提取行号和列号。参考如下补丁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
--- /home/majiang/.vscode/extensions/notskm.clang-tidy-0.4.1/out/tidy.js
+++ /home/majiang/.vscode/extensions/notskm.clang-tidy-0.4.1/out/tidy-fix.js
@@ -97,6 +97,7 @@
"FilePath": diag.DiagnosticMessage.FilePath,
"FileOffset": diag.DiagnosticMessage.FileOffset,
"Replacements": diag.DiagnosticMessage.Replacements,
+ "Lineno": 0,
"Severity": vscode.DiagnosticSeverity.Warning
}
});
@@ -109,6 +110,7 @@
"FilePath": diag.FilePath,
"FileOffset": diag.FileOffset,
"Replacements": diag.Replacements ? diag.Replacements : [],
+ "Lineno": 0,
"Severity": vscode.DiagnosticSeverity.Warning
}
});
@@ -117,7 +119,8 @@
let diagnostics = structuredResults.Diagnostics;
const severities = collectDiagnosticSeverities(clangTidyOutput);
for (let i = 0; i < diagnostics.length || i < severities.length; i++) {
- diagnostics[i].DiagnosticMessage.Severity = severities[i];
+ diagnostics[i].DiagnosticMessage.Severity = severities[i].severity;
+ diagnostics[i].DiagnosticMessage.Lineno = severities[i].lineno;
}
return structuredResults;
}
@@ -129,10 +132,9 @@
if (diagnosticMessage.Replacements.length > 0) {
diagnosticMessage.Replacements
.forEach(replacement => {
- const beginPos = document.positionAt(replacement.Offset);
- const endPos = document.positionAt(replacement.Offset + replacement.Length);
+ const line = Number(diagnosticMessage.Lineno) - 1;
const diagnostic = {
- range: new vscode.Range(beginPos, endPos),
+ range: new vscode.Range(line, 0, line, Number.MAX_VALUE),
severity: diagnosticMessage.Severity,
message: diagnosticMessage.Message,
code: diag.DiagnosticName,
@@ -142,7 +144,7 @@
});
}
else {
- const line = document.positionAt(diagnosticMessage.FileOffset).line;
+ const line = Number(diagnosticMessage.Lineno) - 1;
results.push({
range: new vscode.Range(line, 0, line, Number.MAX_VALUE),
severity: diagnosticMessage.Severity,
@@ -157,28 +159,28 @@
exports.collectDiagnostics = collectDiagnostics;
function collectDiagnosticSeverities(clangTidyOutput) {
const data = clangTidyOutput.split('\n');
- const regex = /^.*:\d+:\d+:\s+(warning|error|info|hint):\s+.*$/;
+ const regex = /^.*:(\d{1,})+:(\d{1,})+:\s+(warning|error|info|hint):\s+.*$/;
let severities = [];
data.forEach(line => {
const matches = regex.exec(line);
if (matches === null) {
return;
}
- switch (matches[1]) {
+ switch (matches[3]) {
case 'error':
- severities.push(vscode.DiagnosticSeverity.Error);
+ severities.push({severity: vscode.DiagnosticSeverity.Error, lineno: matches[1]});
break;
case 'warning':
- severities.push(vscode.DiagnosticSeverity.Warning);
+ severities.push({severity: vscode.DiagnosticSeverity.Warning, lineno: matches[1]});
break;
case 'info':
- severities.push(vscode.DiagnosticSeverity.Information);
+ severities.push({severity: vscode.DiagnosticSeverity.Information, lineno: matches[1]});
break;
case 'hint':
- severities.push(vscode.DiagnosticSeverity.Hint);
+ severities.push({severity: vscode.DiagnosticSeverity.Hint, lineno: matches[1]} );
break;
default:
- severities.push(vscode.DiagnosticSeverity.Warning);
+ severities.push({severity: vscode.DiagnosticSeverity.Warning, lineno: matches[1]});
break;
}
});