使用R语言的scope参数指定候选预测变量

90 篇文章 ¥59.90 ¥99.00
本文介绍了R语言中如何使用scope参数来指定候选预测变量,确保在函数内部正确访问外部变量。通过示例解释了scope参数如何避免解析规则导致的意外结果,以及如何与其他方法结合使用来管理变量作用域。

使用R语言的scope参数指定候选预测变量

在R语言中,我们经常需要使用函数来处理数据和执行各种操作。有时候,函数的参数可能会引用外部的变量,这些变量可能在函数内部定义,也可能在函数外部定义。在函数内部访问外部变量时,我们可以使用scope参数来指定候选预测变量,以确定变量的作用域和解析规则。

scope参数的作用是告诉R解释器在解析函数内部的变量时应该考虑的作用域。通常情况下,R会首先查找函数内部的局部变量,如果找不到,则会继续查找函数外部的全局变量。然而,当存在嵌套函数或函数内部定义了与外部变量同名的局部变量时,R的解析规则可能会导致意外的结果。

下面我们通过一个简单的示例来说明scope参数的使用。假设我们有一个外部变量x,并定义了一个函数f,该函数内部定义了一个与外部变量同名的局部变量x。我们希望在函数内部访问到外部的x,而不是局部的x。这时,我们可以使用scope参数来显式指定候选预测变量。

x <- 10

f <- function(x, scope = parent.frame()) {
  # 使用scope参数指定候选预测变量
  eval(substitute(x), envir = scope)
}

f(20)  # 输出 10

在上面的代码中,我们定义了全局变量x,并在函数f内部定义了同名的局部变量x。在函数f内部,我们使用了eval和substitute函数来访问外部的x。通过将scope参数设置为parent.frame(),我们告诉R解析器在解析x时应该考虑函数外部的作用域。因此,函数f内部的x实际上引用的是外部的全局变量x,而不是局部变量x。

<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 } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值