发布者: Renjian

前阵子基于google的cpplint实现了检查我们自己项目coding style的lint工具,本文对cpplint相关知识进行汇总(注:本文中的代码为部分代码,不能直接运行)。 本文分为两个部分,cpplint代码结构解析和改造cpplint。

代码结构解析

cpplint主要从两个方面对代码进行检测

  • 基于行,如空格,特殊字符,类型转换等。 
  • 基于代码块,如namespace,函数,类等。 

对以上两个方面的检测间接决定cpplint的基本思想,即逐行扫描文件,并记录文件的代码块信息,同时进行检测,将检测结果通过error函数输出。下图通过函数调用关系介绍了代码的基本结构。

其主干线为 ParseArguments()--->processFile()-->ProcessFileData()--> ProcessLine() -->PrintErrorCounts()

cpplint 工作流程

下图为cpplint的类图,Cpplint输入为文件名,输出lint信息。

  • CppLintState()为核心类,cpplint通过它管理其他类进行check
  • FileInfo记录文件的类型,长度等基本信息,cpplint根据这些基本信息来决定是否检测该文件
  • CleanSedLines()记录文件的行信息,用来进行基于行的检测及基于代码块检测的预处理
  • 其他类主要用来储存代码块的信息,用来进行代码块的检测 

cpplint classes relationship

改造cpplint

本部分主要介绍针对cpplint的改造,使之满足项目的特定需要。

  • 以前只支持输入文件,现在支持输入文件夹,检测文件夹下面所有文件,在main函数里对filename进行再处理
for filename in filenames:
    if os.path.isdir(filename):
      for root, _dirs, files in os.walk(filename):
        for subfile in files:
          subfilename=os.path.join(root,subfile)
          ProcessFile(subfilename, _cpplint_state.verbose_level)
    else:
      ProcessFile(filename, _cpplint_state.verbose_level)
  • 跟perforce相结合添加changelistdatafile参数, 检测特定changelist里面新添加的文件(不包括move file)。基本思想为获取changelist里面的文件,将add的文件组成一个set,delete的文件组成一个set.检测add文件set里面的文件,其文件名不能出现在delete set中。

  • 添加–datafile参数,检测配置文件里记录的文件

elif opt == '--changelist':
      changelist_num = val
      filenames_changelist = GetFilenamesFromChangeList(changelist_num, p4envlist)
      filenames += filenames_changelist
    elif opt == '--datafile':
      datafile = val
      filenames_datafile = GetFilenamesFromDataFile(datafile, p4envlist)
      filenames += filenames_datafile
    elif ...:


    def GetFilenamesFromDataFile(datafile, p4envlist):
    """Get filenames from existing data file
    """
        depot_data_file = open(datafile, 'r')


        # ConvertDepotPathToWorkPath() transform the depot path in perforce to local path
        linted_data_filenames = set(ConvertDepotPathToWorkPath(depot_filename.rstrip('\n'), p4envlist) 
                for depot_filename in depot_data_file if depot_filename != '\n')
    
        return list(linted_data_filenames)


    def UpdateDataFileFromChangeList(linted_file, changelist_num, p4envlist):
    """Update From specifical changelist
        Add new added file to datafile
        Input: [linted_file, changelist_num, p4envlist]
        Return: [update_files, is_linted_file_changed]
    """

        linted_filenames = set(linted_filename.rstrip('\n') for linted_filename in linted_file)
        if p4envlist:
            describe_output = subprocess.Popen(('p4', '-p', p4envlist[0], '-u', p4envlist[1],
                            '-c', p4envlist[2], 'describe', '-s', changelist_num),
                            stdout=subprocess.PIPE).stdout)
            checked_file_lines = describe_output.readlines()
            describe_output.close()


            if 'Affected files ...\n' in checked_file_lines:
                begin_index = checked_file_lines.index('Affected files ...\n')
                checked_file_lines = checked_file_lines[begin_index+2:-1]

        else:
            opened_output = subprocess.Popen(('p4', 'opened', '-c', changelist_num),
                            stdout=subprocess.PIPE).stdout)
                             checked_file_lines = opened_output.readlines()
            opened_output.close()

        update_files = set()
        add_depot_files = set()
        delete_depot_files = set()

        for line in checked_file_lines:
            line = line.strip()
            if not line:
                continue

            if p4envlist:
                m_line = re.search(r'\.\.\. (?P<depot_path>[^#]+)#\d+ (?P<file_opt>add|delete|edit)', line)
            else:
                m_line = re.search(r'(?P<depot_path>[^#]+)#\d+ - (?P<file_opt>add|delete|edit) .*', line)
        if not m_line:
            continue

        depot_file = m_line.group('depot_path')
        ....

        if os.path.splitext(depot_file)[-1] not in ('.c', '.cc', '.cpp', '.cxx', '.h', '.hpp'):
            continue

        if m_line.group('file_opt') == 'add':
            add_depot_files.add(depot_file)
        elif m_line.group('file_opt') == 'delete':
            delete_depot_files.add(depot_file)
        elif depot_file in linted_filenames and not p4envlist:
            update_files.add(ConvertDepotPathToWorkPath(depot_file))

    delete_filenames = {}
    for delete_depot_file in delete_depot_files:
        delete_work_file = ConvertDepotPathToWorkPath(delete_depot_file, p4envlist)
        delete_filename = os.path.basename(delete_work_file)
        delete_filenames[delete_filename] = delete_depot_file

    is_linted_file_changed = False
    for add_depot_file in add_depot_files:
        add_work_file = ConvertDepotPathToWorkPath(add_depot_file, p4envlist)
        add_filename = os.path.basename(add_work_file) 

        if add_filename in delete_filenames:
            if delete_filenames[add_filename] not in linted_filenames:
                continue

    update_files.add(add_work_file)
    if add_depot_file not in linted_filenames:
        linted_file.write(add_depot_file+'\n')
        is_linted_file_changed = True

    return [update_files, is_linted_file_changed]
  • 添加--custom参数,支持自定义check rule

cpplint.py提供--filter参数来实现lint类型的过滤. cpplint规定一系列lint的类型,whitespace, +whitespace/bracesruntime/printf, +runtime/printf_format

默认情况下cpplint检查所有类型(详细内容可参考代码,已在其他文中介绍),用户可以通过filter=-x,+y,...定制自己的lint需求

"-FOO"  means "do not print categories that start with FOO".
    "+FOO" means "do print categories that start with FOO".
    Examples: --filter=-whitespace,+whitespace/braces,+build/include_what_you_use

cpplint提供了--customstyle参数,通过--customstyle参数,用户可以输入一个以正则表达式形式表现的check rule, 通过这个rule去check目标文件的每一行. 由于在customstyle参数输入正则表达式的时候不支持空格,用户可以以两个叹号(’!!’)进行替换,程序内部会将两个叹号转换成空格然后进行正则检测。 例如: python cpplint.py --customstyle=void.*!! $file

实现代码如下:

_CUSTOM_STYLE_TEMPLATES = []
    ... elif opt == '--customstyle':
    input_style = val.replace('!!', ' ')
    global _CUSTOM_STYLE_TEMPLATES
    _CUSTOM_STYLE_TEMPLATES.append(input_style)
        global _CUSTOM_STYLE_TEMPLATES...

    def CheckCustomStyle(filename, clean_lines, linenum, error):
    """Check for custom style which users defined.
    Args:
        filename: The name of the current file.
        clean_lines: A CleansedLines instance containing the file.
        linenum: The number of the line to check.
        error: The function to call with any errors found.
    """
    line = clean_lines.elided[linenum]
    for custom_style in _CUSTOM_STYLE_TEMPLATES:
        if Search(r'%s'%custom_style, line)::q
            error(filename, linenum, 'readability/braces', 5,
                'this line can not follow the custom style: %s'%custom_style)

与其他工具的结合

cpplint作为代码检查工具,可以和其他工具相结合,如perforce, Visual Studio, eclipse等, 其他方法类似,这里不再赘述。

  • Linux: 进入cpplint.py所在目录,执行如下命令
optional: p4 login
    python cpplint.py --changelist='195822'

上面code表示对ChangeList 195822中的文件进行代码检查,目前仅对新加的文件进行检查。

  • Windows: 在perforce点击Tools-Manage Custom Tools…上面进行设置,如图:

wino

wint

选择Tool,按下图进行设置,Application 选择同步下来的cpplint.bat文件,点击OK,就可以在P4中使用cpplint.

winth

winf

在perforce中使用cpplint跟post-review一样,右键changelist, 点击cpplint,即可以看到cpplint的结果,如下:

winfi



-EOF-
睿初科技软件开发技术博客,转载请注明出处

blog comments powered by Disqus