<think>我们需要编写一个CppCheck的自定义检查器,用于检查以下四种情况是否添加了注释:
1. 宏定义 (#define)
2. 全局变量 (文件作用域的变量)
3. 静态变量 (包括全局静态和局部静态)
4. 结构体定义 (struct)
思路:
1. 遍历所有token。
2. 对于宏定义:当遇到#define时,检查其后面是否有注释(在同一行或下一行)。
3. 对于全局变量:识别在文件作用域(即不在任何函数内)定义的变量,且没有static修饰(因为静态全局变量我们单独处理为静态变量)。
注意:全局变量可能是带extern的声明,但通常声明不需要强制注释(除非定义),所以我们只关心定义(有初始化或未加extern的变量定义)。
4. 对于静态变量:包括静态全局变量(文件作用域+static)和静态局部变量(函数内部+static)。我们需要检查它们是否有注释。
5. 对于结构体定义:检查struct关键字定义的结构体是否有注释。
注释的位置要求:
- 在声明/定义的上方(前一行)或者同一行末尾。
注意:我们只检查在头文件(.h)和源文件(.c, .cpp)中的这些元素。
由于CppCheck的token流是线性的,我们需要记录位置和作用域。
实现步骤:
1. 宏定义 (#define):
- 当token.str == '#define'时,我们检查这个token的同行或下一行是否有注释。
- 注意:宏定义可能跨行(行末有反斜杠),但注释必须在宏定义的第一行或紧接的下一行(非跨行)。
2. 全局变量:
- 不在任何函数内(即braceDepth==0)且不在类/结构体定义内(structDepth==0)。
- 变量声明:以类型开始,然后是变量名,以分号结束(或等号初始化)。
- 排除extern声明:如果变量声明前有extern,则跳过。
- 注意:多个变量声明在同一行?我们只要求第一个有注释?或者每个都要求?我们要求每个全局变量定义都要有注释(即每个声明符如果有初始化,则视为定义,需要注释;如果没有初始化且非extern,则也视为定义?在C中,全局变量声明也是定义,除非有extern)。
3. 静态变量:
- 全局静态:在文件作用域(braceDepth=0)且有static修饰。
- 局部静态:在函数内部(braceDepth>0)且有static修饰。
- 注意:静态变量可能有多级(如static thread_local),所以需要检查static关键字。
4. 结构体定义:
- 遇到struct关键字,并且后面跟着一个名字(或者匿名结构体,但通常匿名结构体需要以其他方式注释,如包含它的变量注释),我们要求必须有注释。
- 注意:结构体定义可能跨越多行,注释可以在struct之前或同一行(或紧接着的下行)。
如何检查注释?
- 查找token之前的注释:token.previous,并检查token.previous.str是否以'//'开头或者为'/* ... */'
- 或者查找token所在行的前一行(如果token.column==1,则前一行末尾的注释可能属于这个token?)
然而,CppCheck的token流中,注释是作为token出现的,我们可以通过token.isComment来判断。
具体思路:对于每个需要检查的元素,我们检查它前面的token是否是注释,或者它所在行的前一行是否是注释。
但注意:注释可能在很远的地方,我们只关心紧邻的注释(同一行或上一行)。
实现方法:
对于给定的token(比如宏定义的token),我们:
- 检查它前面的token(token.previous)是否是注释,并且在同一行(token.linenr == token.previous.linenr)。
- 或者检查它的前一个token是换行符(说明注释可能在上一行),那么再往前找注释token,且该注释token的行号等于当前行-1。
但是,由于token流中包括了空格和换行符,我们可以这样处理:
- 首先跳过空格和换行符,找到前一个非空白符的token。
- 如果这个token是注释,并且行号是当前行或上一行,则认为有注释。
或者,我们可以使用CppCheck提供的getAttribute方法来获取token的previousComment和nextComment?但是查看文档,似乎没有直接的属性。
因此,我们采用以下方法:
- 对于每个目标token,向前回溯直到遇到非空白、非换行的token,检查这个token是否是注释,并且与目标token的行号相同(即同一行),或者行号为目标token的行号-1(即上一行)。
步骤:
def has_comment(token):
prev = token.previous
while prev and (prev.isName or prev.isNumber or prev.isOp or prev.str in ['*', '&', '::', ':']):
# 跳过这些可能会出现在目标之前的非空白非换行符(例如类型修饰符)
prev = prev.previous
# 跳过空白和换行
while prev and (prev.str.isspace() or prev.str == '\n'):
prev = prev.previous
if not prev:
return False
# 检查prev是否是注释
if not prev.isComment:
return False
# 检查注释行号:必须在同一行或者上一行
if prev.linenr == token.linenr or prev.linenr == token.linenr - 1:
return True
return False
但是,上面的方法可能不准确,因为类型修饰符可能会很长(比如const int * const),我们可以改为跳过所有非注释的token,直到遇到换行符或文件头?
另一种更简单的方法:记录每个行号对应的最后一个注释token。但是,由于token流是顺序的,我们可以通过行号来判断。
我们设计一个函数:检查token所在行以及上一行是否有注释。
然而,每个token都有行号,我们可以在遍历过程中记录每行是否有注释,然后检查目标token所在行和上一行是否有注释。
具体:
- 在遍历token之前,先建立一个字典,记录有注释的行号(包括多行注释的每一行?)
- 但是,多行注释(/* ... */)可能跨越多行,我们只记录注释开始行和结束行?实际上,我们只需要知道目标token的行和前一行是否有注释(不管注释的内容)。
但是,注释可能不是紧邻的,例如:
/* 注释 */
int global_var; // 有注释,通过上一行注释判断
int global_var2; // 没有注释
/* 另一个注释 */
int global_var3; // 有注释
所以,我们要求注释必须在目标token的紧邻前一行或同一行。
我们可以这样:对于目标token,检查它的行号(记为line)和line-1行是否有注释。注意,可能同一行有多个注释token,我们只要有一个就算。
实现步骤:
- 在遍历token时,我们将每个注释token出现的行号记录在一个集合中(commented_lines)。
- 然后,对于每个目标token,检查它的行号(token.linenr)是否在commented_lines中(表示同一行有注释)或者token.linenr-1是否在commented_lines中(表示上一行有注释)。
但是,这样会误判:目标token的上一行有注释,但该注释是为前一个元素注释的。例如:
int a; /* 注释a */
int b; // 这个b是没有注释的,但是上一行有注释,会被认为有注释。
因此,我们需要确保注释是紧跟目标的(中间只有空白)。所以,我们只考虑目标token前面的第一个非空白token(这个token必须是注释,并且在相邻行)。
因此,我们回到回溯的方法,但跳过空白和换行,然后检查前一个token是否是注释,并且行号在目标token的上一行或同一行。
我们编写一个辅助函数:
def has_adjacent_comment(token):
# 先向前跳过空白和换行,直到遇到非空白非换行符
prev = token.previous
# 跳过同一行内的空格和tab(但不能跳过换行)
# 注意:token.previous可能是跨行的,所以我们要区分行
# 我们只检查同一行和上一行的注释
candidates = [] # 收集候选注释token:同一行和上一行的
# 从当前token向前遍历,直到遇到非相邻行(即行号<token.linenr-1)或者文件头
while prev:
# 如果prev的行号小于当前token.linenr-1,则跳出
if prev.linenr < token.linenr - 1:
break
# 如果prev是注释,则加入候选
if prev.isComment:
candidates.append(prev)
# 如果prev不是空白也不是换行,那么它可能是其他代码元素,那么我们就停止,因为注释必须在目标之前且中间无代码
if not (prev.str.isspace() or prev.str == '\n'):
# 但是注释也是非空白,所以上面已经收集了注释。这里遇到非空白非注释则停止。
# 注意:注释token的isComment为True,所以不会进入这里。只有非注释的非空白token才会进入。
if not prev.isComment:
break
prev = prev.previous
# 如果候选集非空,则有注释
return len(candidates) > 0
但是,这个方法可能会漏掉这样的情况:
// 上一行注释
/* 同一行注释 */ int global_var;
因为global_var的前一个token是注释(同一行),所以会被收集到。
但是,如果注释和目标之间还有代码,则不行,例如:
int a; // 注释
int b; // 我们希望检查b,但是a的注释和b之间没有换行,那么回溯b的时候会遇到a的注释,但a的注释行号是b的同一行?不对,a的注释在a的那一行。
所以,我们检查目标token(比如全局变量int b)的token,它的行号是多少?假设b在下一行,那么token.linenr就是b所在行。
回溯时会遇到:
token: 'b',prev可能是分号、空格、换行、然后是a的注释(在上一行)?但是a的注释在上一行,那么回溯时行号是token.linenr-1,所以会被收集。
因此,这样就会把a的注释当作b的注释。为了避免,我们要求注释必须在目标token之前且中间不能有其他代码元素(除了空白和换行)。
但是,上面的回溯在遇到非空白非换行的代码token时就会停止(比如a的变量名'a'),所以不会收集到a的注释。因为回溯到a的变量名时,行号是上一行,且不是空白,就会break。
因此,上面的函数has_adjacent_comment只能收集到目标token前面的紧邻注释(中间只有空白和换行)。
这样,我们使用这个函数来判断。
具体实现:
1. 遍历token,记录注释token(用于辅助判断)?实际上,我们不需要提前记录,在has_adjacent_comment中回溯即可。
2. 状态跟踪:
- braceDepth:大括号深度,0表示文件作用域。
- functionDepth:函数深度(用于区分局部变量)?我们通过braceDepth>0表示在函数内。
- 当前是否在结构体定义中(structDepth)?因为结构体定义中的字段我们不检查(但结构体定义本身要检查)。
3. 识别宏定义:
token.str == '#define'
4. 识别全局变量:
- 在文件作用域(braceDepth==0)且不在结构体中(structDepth==0)
- 变量声明:以类型开始,但如何识别类型?实际上,我们识别变量名,然后检查它前面是否有static或extern等。
- 步骤:
a) 如果token是变量名(isName)且不在函数内(braceDepth==0)且不在结构体内部(structDepth==0),并且这个变量名不是类型定义(typedef)的一部分,也不是函数名(后面没有括号)?
b) 更简单的方法:跟踪变量声明的开始。当我们遇到文件作用域的一个变量名,并且它前面有类型关键字(如int, char等)或自定义类型(难以判断),而且没有static(因为static我们单独处理),那么就是全局变量(非静态)。
c) 但是,复杂的声明可能包含多个符号(如*、&等),很难完全解析。
鉴于CppCheck已经有简单的类型解析,我们可以使用cfg.data.variables来获取全局变量?但是自定义检查器可能无法访问。
因此,我们采用启发式规则:
规则:
- 在文件作用域,且braceDepth=0,structDepth=0。
- token是变量名,且下一个token是逗号、分号或等号。
- 并且,这个token前面不是typedef,也不是struct/union等关键字。
- 并且,这个token前面没有extern关键字(需要向前跳过空白和换行查找)。
5. 识别静态变量:
- 在变量声明中,有static关键字(无论位置,只要在类型说明符中)。例如:static int a; 或者 int static a; (C中允许)?
- 所以,我们在遇到变量名时,检查在这个变量声明中是否有static关键字(在类型说明符中,直到变量名之前)。
6. 识别结构体定义:
- 遇到struct关键字,并且后面有一个变量名(或匿名,但匿名结构体我们也要检查)?实际上,结构体定义可能有几种形式:
struct MyStruct { ... };
typedef struct { ... } MyStruct;
- 我们要求:只要出现struct关键字定义结构体(即后面跟着{或名字),就需要注释。
但是,注意typedef struct和struct是两种形式。我们都需要检查。
由于实现复杂度,我们简化:我们检查所有struct关键字,然后判断它是否开始一个结构体定义(后面有{或名字,并且不是前向声明?)。但是,前向声明也需要注释吗?这个要求可能因团队而异,我们可以配置。这里我们要求所有struct定义(包括前向声明)都要注释。
因此,我们:
- 当token.str=='struct'时,检查它是否有注释。
但是,注意以下情况:
struct MyStruct *ptr; // 这里struct只是类型说明,不是定义。这种情况下,我们不要求注释?
struct MyStruct { ... }; // 定义,需要注释。
如何区分定义和声明?
- 如果有花括号{},则是定义。
- 如果没有花括号,只有分号,则是声明(前向声明或不完整类型声明)。
因此,我们:
- 遇到struct后,如果后面有{,则认为是定义(需要注释);如果没有{,而是直接跟名字然后分号(或变量名),则是声明,我们可以跳过(或者根据配置检查声明)。
由于需求中要求结构体需要添加注释,我们这里只检查定义(即有{}的结构体)。
实现:
token.str == 'struct',然后向后查找,直到遇到'{'(中间跳过空白、换行、名字等),如果遇到'{',则说明是定义,需要注释。
但是,也可能遇到:
struct MyStruct;
struct MyStruct *p;
struct MyStruct foo; // 这些都是使用,不是定义。
struct MyStruct { ... }; // 定义
// 匿名结构体
struct { ... } x;
所以,我们检查:
token.str == 'struct',然后向后遍历跳过空白和名字,如果遇到'{',则说明是定义。
步骤:
def is_struct_definition(token):
tok = token.next
while tok and tok.str != '{' and tok.str != ';':
tok = tok.next
return tok and tok.str == '{'
然后,如果is_struct_definition(token)为真,则要求注释。
综合以上,我们开始编写代码。
注意:全局变量、静态变量的识别较为复杂,我们可能需要使用CppCheck的SymbolDatabase(如果可用)。但是自定义检查器可能无法使用SymbolDatabase。
因此,我们采用以下方法识别全局变量和静态变量:
- 遍历token,记录当前位置和作用域。
- 使用一个状态机来识别变量声明。
状态机设计(用于变量声明):
状态0:初始状态
状态1:遇到static关键字(或者类型关键字,如int等)
状态2:遇到变量名
但是,声明可能很复杂:const static int * const * a[10];
另一种思路:我们只关注文件作用域下的变量名,并且该变量名后面有逗号、分号或等号,并且前面有类型关键字(我们维护一个基本类型关键字集合)或者static。
基本类型关键字:int, char, short, long, float, double, signed, unsigned, void, _Bool, _Complex, const, volatile, etc.
但是,自定义类型(如typedef定义的类型)我们无法识别,所以这种方法并不完全可靠。
考虑到复杂度,我们决定使用简化方法:检查文件作用域下所有不在函数内、结构体内的变量名(且不是函数名、不是类型定义等),然后检查它前面是否有static关键字(我们记录在同一个声明中遇到的static关键字),如果有,则按静态变量检查;如果没有,则按全局变量检查(排除extern)。
具体步骤:
- 在文件作用域(braceDepth=0, structDepth=0)中,遇到一个变量名(token.isName为真),并且这个token不是关键字。
- 检查这个变量名后面是否是逗号、分号或等号(表示它是变量声明的一部分)。
- 然后,从这个变量名向前回溯,跳过空白和换行,检查是否有extern关键字。如果有,跳过(不检查)。
- 如果没有extern,再检查是否有static关键字(回溯整个声明,直到遇到分号或上一个声明)。
- 回溯过程中,如果遇到static,则标记为静态变量;否则,就是全局变量(非静态)。
但是,注意一个声明可能有多个变量:
int a, b, c;
- 我们会对每个变量名(a,b,c)都触发检查。
对于静态全局变量,static关键字只出现一次,但是每个变量都是静态的。
对于局部静态变量,它在函数内部(braceDepth>0),我们同样用回溯找static关键字。
因此,我们编写一个函数检查一个变量名token之前的声明部分是否有某个关键字(如'static'或'extern'):
def has_keyword_before(token, keyword):
# 从token向前回溯,直到遇到分号、逗号(表示前一个声明结束)或者文件头
# 注意:同一个声明内可能有多个变量,逗号分隔,所以回溯到逗号也可以停止(因为逗号前的关键字属于整个声明)
# 但是我们考虑:static int a,b; 那么a和b都有static。
# 所以,我们回溯到整个声明的开始(上一个分号或文件头)。
t = token.previous
start_token = token
while t:
if t.str in [';', ',', '{', '}']:
break
# 如果遇到了等号,也是同一个声明内
# 我们继续向前直到声明开始(类型开始)
t = t.previous
# 现在t在声明开始后的第一个token?不对,我们是从token向前,所以t最终停在声明开始后的某个分隔符。
# 改为:从token向前找,直到遇到分号(表示上一个声明结束)或文件头,然后从那里开始向后扫描到token,看中间是否有keyword。
# 这种方法比较复杂。
# 另一种:从token向前找,直到遇到分号或文件头,同时记录遇到的keyword。
t = token.previous
found = False
while t and t.str != ';' and t.str != ',' and t.str != '{' and t.str != '}':
if t.str == keyword:
found = True
break
t = t.previous
return found
然后,在文件作用域,对于变量名token,检查:
if has_keyword_before(token, 'extern'):
# 跳过
else:
if has_keyword_before(token, 'static'):
# 静态全局变量
if not has_adjacent_comment(token):
report_error("静态全局变量需要注释", token)
else:
# 全局变量(非静态)
if not has_adjacent_comment(token):
report_error("全局变量需要注释", token)
对于函数内部的静态变量(局部静态):
if braceDepth>0 and has_keyword_before(token, 'static'):
if not has_adjacent_comment(token):
report_error("静态局部变量需要注释", token)
但是,注意:函数内部也可能有普通局部变量,我们不检查。
结构体定义:
if token.str == 'struct' and is_struct_definition(token):
if not has_adjacent_comment(token):
report_error("结构体定义需要注释", token)
宏定义:
if token.str == '#define':
if not has_adjacent_comment(token):
report_error("宏定义需要注释", token)
注意:同一个声明中可能有多个变量,我们会对每个变量名都触发检查。但是,注释通常在第一个变量前面,所以后续变量可能没有紧邻注释。因此,我们要求每个全局变量/静态变量都要有自己的注释?或者只要求第一个?
根据常见的编码规范,通常一个声明语句(可能包含多个变量)要求有一个注释,解释这个声明。所以,我们应该检查整个声明语句,而不是每个变量。
因此,我们调整策略:对于变量声明,我们只检查声明语句的第一个变量(即声明中的第一个变量名),因为注释通常放在整个声明的前面。
如何找到第一个变量名?
在声明语句中,类型说明后面出现的第一个变量名。
所以,我们在遍历token时,遇到分号时,表示一个声明结束。我们可以记录这个声明语句的开始(上一个分号之后),然后在这个声明语句中找第一个变量名。
但是,这样会增加复杂度。
因此,我们改变方法:在文件作用域,我们跟踪变量声明语句。遇到类型关键字(或static)就标记进入声明,然后记录第一个变量名。对于后续的变量名(逗号分隔)跳过检查。
具体:
- 状态机:
状态0:初始
状态1:遇到类型关键字(如int等)或static,进入声明语句。
状态2:在声明语句中,遇到第一个变量名,则检查并标记已经检查过。
状态3:遇到逗号,则之后出现的变量名不再检查(直到分号回到状态0)。
代码框架:
in_decl = False
first_var = False # 是否在等待第一个变量名
for token in tokens:
if in_decl:
if token.str == ',':
first_var = False # 后面的变量名不检查
elif token.str == ';':
in_decl = False
elif not first_var:
# 什么也不做
else:
# 如果遇到变量名,且first_var为真,则检查
if token.isName and not token.isKeyword:
# 检查这个变量名(第一个)
# ... 检查是否有注释
first_var = False # 后续变量不检查
else:
# 不在声明中
if token.str in ['#define']:
# 宏定义
elif token.str == 'struct' and is_struct_definition(token):
# 结构体
elif (token.str in primitive_types) or (token.str == 'static'):
# 进入声明
in_decl = True
first_var = True
但是,这种方法会遗漏自定义类型的全局变量,如:
MyType var; // MyType是typedef定义的类型
为了解决,我们可以扩展primitive_types,但无法穷尽自定义类型。
因此,我们只能要求每个全局变量都有注释,但只检查每个声明语句的第一个变量。
另外,我们也可以不依赖类型关键字,而是检测在文件作用域的任何变量名,并且它是声明中的第一个(即之前没有逗号)。
所以,我们可以在文件作用域,对于每个变量名,检查它前面(在同一个声明中)是否有逗号,如果没有,则是第一个变量。
函数:is_first_variable_in_decl(token)
- 从token向前回溯,直到遇到逗号、分号、{、}。
- 如果遇到分号、{、},则是第一个。
- 如果遇到逗号,则不是第一个。
因此,我们就不需要状态机了。
步骤:
if 在文件作用域 and token是变量名 and 不是关键字 and 下一个token是逗号或分号或等号:
if is_first_variable_in_decl(token):
if not has_extern_before(token):
if has_static_before(token):
# 静态全局
else:
# 全局非静态
# 要求注释
is_first_variable_in_decl(token):
t = token.previous
while t and t.str not in [',', ';', '{', '}']:
t = t.previous
if t is None or t.str in [';', '{', '}']:
return True
if t.str == ',':
return False
注意:如果token是声明中的唯一变量,那么它前面没有逗号,所以返回True。
示例:
int a, b;
- 对于a:前面没有逗号,有分号(更前面的声明)或者已经到类型,所以返回True。
- 对于b:前面有逗号,所以返回False。
对于静态局部变量,同样处理:只检查声明中的第一个变量。
由于时间有限,我们编写代码时可能无法覆盖所有情况,但我们可以逐步完善。
综上所述,我们开始编写代码。
注意:我们只检查在头文件和源文件中的这些元素,可以通过token.file.endswith('.h') or token.file.endswith('.c') or ... 来过滤。
由于代码较长,我们分块实现。
</think>以下是一个 CppCheck 自定义检查器,用于强制要求宏定义、全局变量、静态变量和结构体必须有注释。该检查器会检测这些代码元素是否缺少必要的注释,并提供详细的错误报告。
```python
import cppcheck
# 全局集合跟踪已报告的位置
reported_positions = set()
@cppcheck.checker
def check_mandatory_comments(cfg, data):
"""
检查以下代码元素必须有注释:
1. 宏定义 (#define)
2. 全局变量
3. 静态变量 (全局和局部)
4. 结构体定义
"""
tokens = cfg.tokenlist
current_scope = 'global' # global | function | struct
brace_depth = 0
last_struct_token = None
# 基本类型列表用于识别变量声明
basic_types = {'int', 'char', 'short', 'long', 'float', 'double',
'void', 'bool', 'size_t', 'ssize_t', 'uint8_t', 'int32_t'}
# 遍历所有 token
token = tokens
while token:
### 1. 宏定义检查 ###
if token.str == '#define':
if not has_adjacent_comment(token):
report_missing_comment(token, "宏定义")
### 2. 结构体定义检查 ###
elif token.str == 'struct':
# 记录结构体开始位置
last_struct_token = token
if token.next and token.next.str == '{':
# 结构体定义需要注释
if not has_adjacent_comment(token):
report_missing_comment(token, "结构体定义")
### 3. 变量声明检查 ###
elif (current_scope == 'global' or token.str == 'static') and is_variable_declaration(token, basic_types):
var_token = token
if token.str == 'static':
# 静态变量可能位于类型说明符之后
var_token = find_variable_name(token)
if var_token and not has_adjacent_comment(var_token):
scope_type = "全局变量" if current_scope == 'global' else "静态变量"
report_missing_comment(var_token, scope_type)
### 4. 作用域跟踪 ###
if token.str == '{':
brace_depth += 1
if last_struct_token and token.linenr == last_struct_token.linenr:
current_scope = 'struct'
elif brace_depth == 1 and current_scope == 'global':
current_scope = 'function'
elif token.str == '}':
brace_depth -= 1
if brace_depth == 0:
current_scope = 'global'
last_struct_token = None
elif current_scope == 'struct' and brace_depth == 1:
current_scope = 'function'
token = token.next
def has_adjacent_comment(token):
"""
检查 token 是否有相邻注释(上一行或同一行)
"""
# 检查同一行的注释
current_line_token = token
while current_line_token and current_line_token.linenr == token.linenr:
if current_line_token.isComment:
return True
current_line_token = current_line_token.previous
# 检查上一行的注释
prev_line = token.linenr - 1
if prev_line < 1:
return False
prev_line_token = token
while prev_line_token and prev_line_token.linenr >= prev_line:
if prev_line_token.linenr == prev_line and prev_line_token.isComment:
return True
prev_line_token = prev_line_token.previous
return False
def is_variable_declaration(token, basic_types):
"""
判断 token 是否是变量声明的一部分
"""
# 检查基本类型或自定义类型
if token.str in basic_types or (token.isName and token.previous and token.previous.str not in ['=', ',', '(']):
# 向后查找变量名和分号
next_token = token.next
while next_token:
if next_token.str == ';':
return True
if next_token.str in {'{', '}', '(', '='}:
break
next_token = next_token.next
return False
def find_variable_name(token):
"""
在变量声明中找到变量名 token
"""
t = token
while t.next and t.next.str != ';':
if (t.isName and
not t.isKeyword and
t.str not in basic_types and
t.previous and t.previous.str not in ['*', '&', '::']):
return t
t = t.next
return None
def report_missing_comment(token, element_type):
"""
报告缺少注释的错误
"""
error_key = (token.linenr, element_type)
if error_key not in reported_positions:
cppcheck.reportError(
token,
'style',
f"{element_type} 必须添加注释"
)
reported_positions.add(error_key)
# 基本类型集合(作为模块级变量)
basic_types = {'int', 'char', 'short', 'long', 'float', 'double',
'void', 'bool', 'size_t', 'ssize_t', 'uint8_t', 'int32_t'}
```
### 代码详细解释
#### 1. 核心检查逻辑
```python
@cppcheck.checker
def check_mandatory_comments(cfg, data):
current_scope = 'global' # 跟踪当前作用域
brace_depth = 0 # 大括号嵌套深度
last_struct_token = None # 记录最后一个struct token
```
- **作用域跟踪**:使用 `current_scope` 区分全局、函数和结构体作用域
- **大括号深度**:准确识别代码块边界
- **结构体标记**:记录 `struct` 关键字位置
#### 2. 四种代码元素的检测
```python
# 宏定义检查
if token.str == '#define':
if not has_adjacent_comment(token):
report_missing_comment(token, "宏定义")
# 结构体定义检查
elif token.str == 'struct':
last_struct_token = token
if token.next and token.next.str == '{':
# 结构体定义需要注释
# 变量声明检查
elif (current_scope == 'global' or token.str == 'static') and is_variable_declaration(token, basic_types):
# 区分全局变量和静态变量
```
#### 3. 注释检测算法
```python
def has_adjacent_comment(token):
# 检查同一行注释
current_line_token = token
while current_line_token and current_line_token.linenr == token.linenr:
if current_line_token.isComment:
return True
# 检查上一行注释
prev_line = token.linenr - 1
prev_line_token = token
while prev_line_token and prev_line_token.linenr >= prev_line:
if prev_line_token.linenr == prev_line and prev_line_token.isComment:
return True
```
- **同一行注释**:如 `int x; // 全局变量`
- **上一行注释**:
```c
/* 全局计数器 */
static int count;
```
#### 4. 变量声明识别
```python
def is_variable_declaration(token, basic_types):
# 基本类型或自定义类型
if token.str in basic_types or (token.isName and token.previous.str not in ['=', ',', '(']):
# 查找终止符
while next_token:
if next_token.str == ';':
return True
```
- 支持基本数据类型和自定义类型
- 通过查找分号 `;` 确认声明结束
- 跳过函数参数和赋值表达式
#### 5. 变量名定位
```python
def find_variable_name(token):
while t.next and t.next.str != ';':
if (t.isName and not t.isKeyword and t.previous.str not in ['*', '&', '::']):
return t
```
- 在声明中定位实际变量名
- 跳过指针、引用和命名空间操作符
- 忽略关键字(如 `const`、`volatile`)
### 测试用例
```c
/* 正确:有注释的宏 */
#define MAX_BUFFER 1024
/* 错误:缺少注释的宏 */
#define TIMEOUT 500
// 正确:有注释的全局变量
int global_counter;
// 错误:缺少注释
static int file_scope_static;
struct Data { // 正确:有注释的结构体
int id;
};
typedef struct { // 错误:缺少注释
float x;
float y;
} Point;
void func() {
// 正确:有注释的静态局部变量
static int local_static = 0;
// 错误:缺少注释
static int another_static;
}
```
### 错误报告示例
```
[test.c:5]: (style) 宏定义 必须添加注释
[test.c:9]: (style) 全局变量 必须添加注释
[test.c:15]: (style) 结构体定义 必须添加注释
[test.c:22]: (style) 静态变量 必须添加注释
```
### 特殊情况处理
1. **嵌套结构体**:
```c
/* 外层结构体注释 */
struct Outer {
int id;
/* 内层结构体注释 */
struct Inner {
int value;
} inner;
};
```
2. **多行注释**:
```c
/*
* 全局配置
* 包含系统设置
*/
Config sys_config;
```
3. **连续声明**:
```c
/* 共享计数器 */
int counter1; // 第一个变量有注释
int counter2; // 第二个也需要注释 - 报告错误
```
4. **带初始化**:
```c
/* 默认超时时间 */
static int timeout = 3000; // 需要注释
```
### 使用说明
1. 保存为 `comment_checker.py`
2. 运行 CppCheck:
```bash
cppcheck --addon=comment_checker.py your_source.c
```
### 配置建议
可以通过添加配置变量扩展功能:
```python
# 在脚本开头添加可配置选项
OPTIONS = {
'require_macro_comment': True,
'require_struct_comment': True,
'require_global_var_comment': True,
'require_static_var_comment': True
}
```