65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ 的静态分析工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (30投票s)

2019年10月7日

GPL3

23分钟阅读

viewsIcon

117081

downloadIcon

2386

自动化Scott Meyers的建议,清理#include指令,分析依赖关系...

引言

C++ 是一种大型语言——有些人会认为它太大了。因为它是 C 语言的超集,所以有 C 背景的开发者很容易构建一个混合的 OO/非 OO 系统。C++ 也保留了预处理器,有时它被用于只能用“卑鄙”来形容的方式。C++ 标准委员会似乎非常不愿弃用任何东西,以免冒犯遗留系统——但却毫不犹豫地不断添加一个又一个看似学究气的功能,至少对于我们这些努力跟上的人来说是这样。

所有这些导致了 C++ 中做某事通常有多种方式,而找出最佳方式可能很困难。如果没有指导,只能通过痛苦的经验才能学到。因此,关于 C++ 最佳实践的书籍有很多也就不足为奇了,例如 Scott Meyers 的《Effective C++》及其续集。但是,当你沉浸在编码中时,尤其是在刚接触这门语言时,很容易忘记他们的建议。当然,有些开发者甚至不屑于阅读这类书籍,他们属于“如果它能工作,它就是**正确**的——所以别碰它!”的学派。如果有一个工具可以充当自动化的 Scott Meyers 代码检查器,将大大有助于解决这些问题。

背景

当我开始开发 Robust Services Core (RSC) 时,我对 C++ 有一定的了解,但远未精通。代码有机地增长,并不断重构。随着我对 C++ 越来越熟悉,并且需要重新访问那些已经休眠了一段时间的代码区域时,我不断发现我现在会以不同方式处理的事情。但是总是有更多的代码要开发,而且从来没有足够的时间进行繁琐的代码检查来找出并“修复”所有可以改进的地方。

最终我决定,至少,清理所有 ` #include ` 指令会很好。肯定有一个公开可用的工具可以做到这一点。那是大约2013年,我发现的唯一工具是 Google 的一项名为“Include What You Use”的倡议,它似乎已经被搁置了。1 因此,我决定编写这样一个工具,作为 RSC 主要工作的消遣。

真是个消遣!很快就发现,修复 `#include` 列表,添加应该有的指令并删除不应该有的指令,意味着要编写一个解析器。而且不仅仅是一个解析器,更接近于一个编译器,因为它还必须进行名称解析和其他事情。另一个选择是获取一个开源的 C++ 编译器,然后修改它,或者从它可能生成的文件中提取必要的信息。

我没有放弃,而是决定从头开始编写这个工具。即使最终不得不放弃,它也会是一次学习经历。本文描述了所产生代码的当前状态。

Using the Code

该代码不仅清理了`#include`指令,还作为一个自动化的Scott Meyers代码检查器,可以通过适当地编辑源代码来实现其许多建议。它的主要缺点是只支持RSC使用的C++11子集。尽管这是该语言的合理子集,但缺失的内容将阻碍其对使用不受支持语言功能的项目的有用性。添加这些缺失的语言功能之一可能从适度容易到相当具有挑战性。尽管如此,请随时请求支持特定的语言功能——甚至自愿实现它!这将使该工具对更广泛的项目有用。

本文的重点是如何使用代码,而不是它如何工作。然而,它最后提供了一个设计的高级概述,为那些想要深入研究代码的人提供了一个路线图。

演练

定义库

在使用该工具之前,必须定义构成代码库的文件。这可以在 RSC 启动后立即从 CLI 输入命令 `>read buildlib` 来完成。那个“`>`”是 RSC 的 CLI 提示符,不应输入,但本文使用它来表示 CLI 命令。所有 CLI 命令的转储可在 *help.cli*2 中找到;向下滚动到大约第 1247 行,“`ct>help full`”,以查看 *ct* 目录中的命令,该工具就在该目录中实现。

`>read buildlib` 的作用是执行脚本 _buildlib_,其中包含一系列 CLI 命令。这将导致以下命令的执行,这些命令从 RSC 生成的控制台记录文件中复制而来,并删除了与本文不相关的命令:

nb>read buildlib
nb>ct
ct>read lib.create
ct>import subs  subs
ct>import nbase nb
ct>import ntool nt
ct>import ctool ct
ct>import nwork nw
ct>import sbase sb
ct>import stool st
ct>import mbase mb
ct>import cbase cb
ct>import pbase pb
ct>import onode on
ct>import cnode cn
ct>import rnode rn
ct>import snode sn
ct>import anode an
ct>import diplo dip
ct>import rsc   rsc

该工具位于 _ct_ 目录中,因此使用命令 `>ct` 访问该目录中的 CLI 命令。然后读取脚本 _lib.create_。它包含一系列 `>import` 命令,这些命令将编译项目(在本例中为 RSC)所需的所有目录添加到代码库中。例如,命令

ct>import ctool ct

导入 *ct* 目录中的代码,随后可以在其他 CLI 命令中将其称为 `ctool`。此目录的路径是相对于 `SourcePath` 配置参数的。RSC 启动时,它从文件 _element.config_ 获取其配置参数。因此,要在您自己的代码上使用这些工具,您需要:

  • 修改 *element.config*,将其 `SourcePath` 条目设置为包含所有项目代码文件的目录。如果未指定 `SourcePath`,则 RSC 本身将被分析。
  • 在 RSC 的 *lib.create* 所在的**同一目录**中创建一个与 *lib.create* 类似的文件。该文件中的每个 `>import` 命令都必须指定一个相对于您的新 `SourcePath` 设置的目录。
  • 将 RSC 中的 _subs_ 目录复制到您自己的项目,紧邻您的 `SourcePath` 目录下方,并在您自己的 *lib.create* 版本中包含命令 `>import subs "subs"`,如 RSC 的 *lib.create* 中所示。
  • 修改 _buildlib_ 脚本以 `>read` 您的 _lib.create_ 版本。

每个 `>import` 命令最终都会为其目录创建一个 `CodeDir` 实例,并为该目录中的每个代码文件3创建一个 `CodeFile` 实例。目前有两个限制:

  • 每个文件名必须是唯一的(即,不能在多个目录中使用相同的名称)。
  • 目录中的所有代码文件都将被导入(即,无法排除代码文件)。

解析代码

导入所有源代码目录后,可以解析整个代码库,这是使用静态分析工具进行检查的先决条件。这通过以下命令完成:

>parse - win64 $files

其中

  • `-` 指定不使用解析器选项(唯一的选项是启用调试工具的选项)
  • `win64` 指定目标是64位Windows(其他目标是`win32`和`linux`)
  • `$files` 是一个内置库变量,包含所有代码文件的集合

如果将 `$files` 替换为 `f ctool`,表示 _ct_ 目录中的所有代码文件,则结果(同样取自控制台日志文件)如下所示:

ct>parse - win64 f ctool
cctype
cmath
csignal
cstdint
cstdio
errno.h
exception
execinfo.h
functional
ioctl.h
iosfwd
mcheck.h
poll.h
resource.h
sched.h
typeinfo
utility
winerror.h
cstddef
endian.h
in.h
ios
process.h
ratio
spawn.h
stdlib.h
system_error
wait.h
atomic
cstdlib
cstring
cxxabi.h
iomanip
iterator
malloc.h
memory
mman.h
new
ostream
pthread.h
queue
socket.h
stack
unistd.h
algorithm
istream
list
map
netdb.h
set
unordered_map
vector
iostream
string
bitset
ctime
filesystem
fstream
sstream
chrono
windows.h
dbghelp.h
intsafe.h
mutex
thread
winsock2.h
condition_variable
ws2tcpip.h
FunctionGuard.h
SysDecls.h
LibraryTypes.h
//
// [many lines deleted]
//
Parser.cpp
  std::chrono::duration<long long,std::ratio<1,10000000>>
  std::chrono::time_point<std::chrono::system_clock,std::chrono::duration<long long,std::ratio<1,10000000>>>
  CodeTools::CxxCharLiteral<char,CodeTools::Cxx::Encoding::ASCII>
  CodeTools::CxxCharLiteral<char16_t,CodeTools::Cxx::Encoding::U16>
  CodeTools::CxxCharLiteral<char32_t,CodeTools::Cxx::Encoding::U32>
  CodeTools::CxxCharLiteral<wchar_t,CodeTools::Cxx::Encoding::WIDE>
  std::iterator_t<const CodeTools::Cxx::Keyword>
  std::unique_ptr<CodeTools::StringLiteral>
  std::move<std::unique_ptr<CodeTools::StringLiteral>>
  std::unique_ptr<CodeTools::Elif>
  std::move<std::unique_ptr<CodeTools::Elif>>
  std::unique_ptr<CodeTools::Else>
  std::move<std::unique_ptr<CodeTools::Else>>
  std::unique_ptr<CodeTools::Endif>
  std::move<std::unique_ptr<CodeTools::Endif>>
  std::unique_ptr<CodeTools::Error>
  std::unique_ptr<CodeTools::Iff>
  std::move<std::unique_ptr<CodeTools::Iff>>
  std::unique_ptr<CodeTools::Ifdef>
  std::move<std::unique_ptr<CodeTools::Ifdef>>
  std::unique_ptr<CodeTools::Ifndef>
  std::move<std::unique_ptr<CodeTools::Ifndef>>
  std::unique_ptr<CodeTools::Line>
  std::unique_ptr<CodeTools::Pragma>
  std::unique_ptr<CodeTools::Undef>
  std::move<std::unique_ptr<CodeTools::Undef>>
Updating cross-reference...
  Total=204, failed=0

解析每个文件时,会显示其名称。模板实例化会缩进(当一个模板导致另一个模板实例化时,会进一步缩进)。

要解析的第一个 RSC 文件是 _FunctionGuard.h_。它之前的文件要么来自标准库、Windows,要么来自 Linux。然而,它们**不是**那些文件的实际实例。相反,它们取自 _subs_ 目录,其中包含它们的简化版本。这些版本避免了以下需求:

  • `>import` 范围广泛的目录中项目外部的文件
  • `#define` 所有在外部文件中正确导航所有 `#ifdef` 所需的名称
  • 支持外部文件使用但项目不使用的 C++ 语言功能
  • 解析项目不使用的许多内容

因此,在您能 `>parse` 您自己的项目之前,您必须确保 _subs_ 目录包含您项目 `#include` 的每个外部头文件的替代文件,并且每个替代文件都声明了您从中使用的项。请注意,对于模板,_subs_ 头文件不需要提供函数定义。

在 `>parse` 过程中,有三种情况可能出错。按严重程度递减的顺序排列,它们是:

  1. **异常**。该工具需要修复,以便在遇到无法解析或编译的内容时能优雅地失败。

  2. **代码无法解析**,因为它使用了解析器不支持的功能。日志会显示解析失败的位置,并且整个文件都无法解析。这可以通过增强解析器或修改代码使其不使用解析器不理解的功能来修复。如果未修复,将导致 `#include` 该失败文件的文件中出现下一类型的问题。

  3. **代码解析成功但无法编译**(例如,通过解析每个名称和函数调用)。同样,日志会描述问题及其发生位置。这可能是工具中的一个 bug,也可能是代码使用了 _subs_ 目录中文件中未声明的某个东西。通常,问题是缺少 `#include`:_subs_ 目录中的文件传递性 `#include` 的内容远少于其实际对应的文件,因此 `#include` 实际声明每个项的文件非常重要。

    这些日志出现的越多,工具在执行代码检查时发出不正确警告的可能性就越大。然而,这些日志的严重性远低于解析器失败。解析器失败意味着文件中没有任何内容被理解。但在这里,只有日志中提到的**代码行**未被理解。

执行代码检查

现在所有代码都已解析,可以检查是否存在违反设计准则的情况:

>check rsc $files

这会生成文件 _rsc.check_,其中包含所有找到的警告。`>check` 可以生成的每个警告的基本文档可以在文件 *cppcheck* 中查看。您会发现它们不是静态分析工具发出的那种常见的“指针可能为空”或“混合有符号和无符号整数”类型的警告,那些工具大多将代码视为普通的 C 语言进行分析。相反,这个工具更侧重于 C++ 最佳实践,例如 Rule of 5 违规,以及突出显示可以为 `const`、`protected` 或 `private` 的内容。

如果您对代码的子集运行 `>check`,它会询问您是否想首先解析成功构建所需的任何未解析代码。解析额外的代码可以避免误报,例如函数未使用或未定义的警告。

在发布新的 RSC 版本之前,我通常会对所有代码运行 `>check`,并使用 VS2022 的 _Git Changes_ 选项卡中的 _diff_ 工具来查看自上次发布以来是否有任何新警告出现。

有两种方法可以抑制警告:

  1. 要抑制警告的**所有**出现,请修改函数 `CodeWarning::Initialize`。通过将构造函数调用中警告属性的第二个 `bool` 参数(`disabled` 标志)设置为 `true` 来更改。
  2. 要仅抑制警告的**部分**出现,请修改函数 `CodeWarning::Suppress`,其中包含许多在 RSC 代码中被抑制的警告示例。

由于 _subs_ 目录中的头文件不提供模板的函数实现,`>check` 可能会错误地建议以下事项:

  • 删除使析构函数对 `unique_ptr` 模板实例可见所需的 `#include`
  • 声明一个数据成员为 `const`,即使它被插入到一个 `set` 中,因此必须允许 `std::move`
  • 删除 _Allocators.h_ 中的大部分内容(它仅由 STL 调用,而不是直接由 RSC 调用)

采纳建议

`>fix` 命令目前能够解决 148 个可能警告中的 101 个。

fix               : Interactively fixes warnings detected by >check.
  (0:148)         : warning number from Wnnn (0 = all warnings)
  (t|f)           : prompt before fixing?
  <str>           : a set of code files

例如,以下命令通过删除不必要的 `#include` 指令(警告 `W018`)来修改所有代码文件。

>fix 18 f $files

要选择要修复的警告出现次数,请要求提示。例如,

>fix 53 t $files

将在修复警告 `W053`(“数据可以为 `const`”)的每次出现之前进行提示。

**警告:**在使用 `>fix` 之前,请确保如果出现问题,您可以恢复文件的原始版本。尽管它定期用于 RSC 的代码,但这并不意味着它已经过彻底测试!

导出库

代码解析后,`>export` 命令可以生成以下文件的任意组合:

  • 一个 _ .lib _ 文件以标准格式显示解析后的代码,包括:

    • 每个 `auto` 变量的底层类型;

    • 每个项被以下次数:

      • 引用,

      • 初始化、读取或写入(对于数据),

      • 调用(对于函数);和

    • 定义每个项的文件(对于数据和函数)。

  • 一个 _ .trim _ 文件列出了每个文件中使用的外部符号,以及关于文件应添加或删除哪些 `#include` 指令、`using` 语句和前向声明的建议。这些建议也作为警告出现在 _ .check _ 文件中。

  • 一个 _ .xref _ 文件包含一个全局交叉引用(每个符号,后跟使用它的文件列表,以及符号出现的行号)。

重命名标识符

`>rename` 命令允许重命名命名空间、类、函数、数据、枚举、枚举器、typedef 和宏名称。其语法是 `>rename` **<旧名称>** **<新名称>**,其中**<旧名称>**可以使用作用域运算符进行限定,或者如果名称不明确,则从提供的列表中选择。

分析代码依赖关系

_ct_ 目录中的许多 CLI 命令都将表达式作为它们的最后一个参数。到目前为止,我们只提到了 `$files`,但表达式可以包含变量和运算符。用户使用 `>assign` 命令定义变量,库还提供了以下不能直接修改的变量:

变量 目录
$dirs 已通过 `>import` 添加到库中的目录
$files `$dirs` 中找到的所有代码文件(头文件和实现文件)
$hdrs `$files` 中的头文件
$cpps `$files` 中的实现文件(_ .c* _)
$subs 声明代码库外部项的头文件
$exts 出现在 `#include` 指令中但其目录未通过 `>import` 添加到库中的头文件(这将导致 `>parse` 失败)
$vars 所有变量(上述变量,以及用户定义的任何变量)

表达式从左到右求值,但可以使用括号来覆盖此规则。变量是一组目录、文件或 C++ 代码项。以下表达式中使用了以下表示法:

Set 目录
D 目录名称(由 `>import` 定义)或目录集
F 特定文件名或文件集
C 特定 C++ 代码项名称或此类项的集合
S 以上任意一项 (D, F, or C)

以下是 `>import` 命令构建库后即可使用的运算符。**表达式**列指定了运算符期望的参数类型。**结果**列是运算符返回的值,该值可用作其他运算符或命令(如 `>assign` 和 `>list`)的输入。

运算符名称 表达式 结果 语义
并集 S1` | `S2 S S1 和 S2 的集合并集(' `|` ' 可选)
交集 S1` & `S2 S S1 和 S2 的集合交集
差集 S1` - `S2 S S1 和 S2 的集合差集
文件 `f `S F S中的文件
目录 `d `S D S中的目录
文件名 F` fn `_<str>_ F F中文件名为 _<str>*_ 的文件
文件类型 F` ft `_<str>_ F F中文件类型为 _*.<str>_ 的文件
匹配项 F` ms `_<str>_ F F中名称部分匹配 _<str>_ 的文件
in F` in `D F F中目录在D中的文件
用户 `us `F F `#include` F 中任何文件的文件
被使用 `ub `F F F 中任何文件 `#include` 的文件
影响者 `as `F F `ub` F,传递性
受影响 `ab `F F `us` F,传递性
共同影响者 `ca `F F `(as` f1`)` `&` `(as` f2`)` `&` … `(as` fn`)`,其中 f1…fn 是 F 中的文件

在 `>parse` 命令运行后,编译后的代码可以使用额外的运算符:

运算符名称 表达式 结果 语义
实现 `im `F F 对于F中声明(定义)的每个项,添加定义(声明)它的文件
需要者 `ns `F F 在构建中也需要F的文件(`im` `ab` F,传递性)
被需要 `nb `F F 在构建中F也需要的文件(`im` `as` F,传递性)
由...声明 `db `S C S中声明的代码项
声明者 `ds `C C 声明C中各项的代码项
定义 `df `C C C中各项的定义(如果与声明分开)
由...引用 `rb `S C S中引用的代码项
引用者 `rs `C C 引用C中各项的代码项

这些运算符的主要目的是分析代码文件和 C++ 项之间的依赖关系。以下是一些简单的示例作为介绍。

在第一张图片中,第一个命令列出了 _Thread.h_ 的使用者。如果在 RSC 的所有代码文件中搜索 `include "Thread.h"`,这些就是您会找到的文件。接下来,_sbase_ 目录中的 _.cpp_ 文件被分配给变量 **sbim**,而可能受 _Thread.h_ 更改影响的文件被分配给 **thrab**。**sbim** 和 **thrab** 的交集被分配给 **sbthr**,并显示结果。这些是 _sbase_ 目录中可能受 _Thread.h_ 更改影响的 _.cpp_ 文件。最后,列出了实现 _Duration.h_ 的文件。_Thread.cpp_ 在那里做什么?!嗯,如果您查看代码,您会发现 _Duration.h_ 中声明的许多常量,甚至在 `main` 进入之前使用,都在 _Thread.cpp_ 的底部初始化,以避免 C++ 臭名昭著的“静态初始化顺序混乱”。4

我们继续将 `Thread` 赋值给 **thr**。但是 `Thread` 可以指代很多东西:`Thread` 类、它的构造函数或它的一个前向声明。所以我们必须指明我们想要构造函数。如果我们输入 `Thread::Thread`,它就不会含糊不清。当我们列出 **thr** 时,我们看到它是 _Thread.h_ 中的一个函数。如果我们列出它的定义,我们看到它的实现从 _Thread.cpp_ 的第 1062 行开始。最后,列出 **thr** 引用的项揭示了 `Daemon` 的前向声明和枚举器 `Faction`。这些类型用于指定 `Thread` 构造函数的参数。

如果我们列出 **thr** 的**定义**所引用的项——也就是构造函数的实现——输出会占满屏幕。

最后,列出对 **thr** 的引用,揭示了对 `Thread` 进行基类构造函数调用的构造函数。

运算符 `db`、`ds`、`df`、`rb` 和 `rs` 允许分析 C++ 代码项(而不仅仅是文件)之间的依赖关系。这可以帮助架构师通过将大型代码库划分为库来分层,以便可以将给定产品不需要的软件从其构建中排除。尽管这些运算符返回 C++ 代码项的集合,但可以通过添加 `f` 运算符前缀轻松找到这些项所在的文件,并且 `db` 和 `rb` 运算符也可以用于文件甚至目录。例如:

`>list f ds C `显示声明 C 中各项的文件
`>list f rs C `显示引用 C 中各项的文件
`>list db F1 & rb F2 `显示在 F1 文件中声明且由 F2 文件引用的项

附加细节

要 #include 什么

添加和删除 `#include` 指令、`using` 语句以及前向声明的警告之间存在交互。`CodeFile::Trim` 生成这些警告。其基本规则是:

  • 如果没有任何东西**保证**它会传递性可见,则始终 `#include` 它。
  • 不要 `#include` 那些**肯定**会传递性可见的东西。有必要 `#include` 一个基类,以及一个直接使用的类。但是,没有必要 `#include` **它们**的基类,即使在使用这些传递性基类中声明的东西时。同样,_.cpp_ 也没有必要 `#include` 任何它的头文件会 `#include` 的东西。
  • 如果一个类只被间接使用(即作为指针或引用类型),不要 `#include` 它。而是使用前向声明。如果没有保证前向声明会传递性可见,请将其添加到此文件中。
  • 头文件不应包含 `using` 指令或声明。因此,它被告知删除它,任何依赖它的 _.cpp_ 文件则被告知添加它。
  • 如果不需要 `#include`、前向声明或 `using` 语句来解析符号,则将其删除。

所有这些警告都可以通过 `>fix` 解决,例如,它将在正确的命名空间中插入一个前向声明,并在删除 `using` 语句时完全限定来自另一个命名空间的符号。

高层设计

解析器是使用递归下降实现的,这使得其代码易于阅读和修改。`unique_ptr` 的出现对于这类解析器来说是个福音,它们以前因为需要回溯时删除对象而备受困扰。将每个对象放入 `unique_ptr` 中,使得解析器可以在不编写任何代码来删除它们的情况下进行回溯。

当每个代码文件在 `>import` 期间读入时,会记录 `#include` 关系。这允许计算全局编译顺序。解析之前发生的唯一其他预处理是,在 C++ 代码中,擦除定义为空字符串的宏名称。一个这样的名称是 `NO_OP`,当 `for` 语句缺少其括号中的某个组件时,RSC 会在裸分号之前或之后使用它。

一旦这种简单的预处理完成,所有代码将一起在一次通过中解析。解析项后,它将被添加到其出现的范围(命名空间、类、函数或代码块)中,并调用其虚拟 `EnterScope` 函数。然后通过调用其虚拟 `EnterBlock` 函数对其进行编译。项的 `EnterScope` 或 `EnterBlock` 函数还会对其每个组成部分调用相同的函数。

该工具不会像完整的编译器那样检查所有内容。它**假设**代码能够正确编译和链接,因此它只包含足够的检查以生成正确的解析。它的语法在相关函数中非正式地记录,比完整的 C++ 语法更简单。

由 `>check` 生成的一些警告是在 `>parse` 期间检测到的,另一些则是在 `>check` 本身通过虚拟函数 `Check` 检测到的。

前一节中提到的 `CodeFile::Trim` 使用虚拟函数 `GetUsages` 从其文件的所有 C++ 项中获取被使用的符号,这些符号 (a) 作为基类,(b) 直接使用,和 (c) 间接使用;通过 (d) 前向声明,(e) 友元声明,和 (f) `using` 语句解析的符号;以及 (g) 继承的符号。

引用通过虚拟函数 `AddToXref` 和 `AddReference` 进行跟踪。这允许 `>export` 创建其交叉引用。在另一个中声明的 C++ 项是从支持 `db` 运算符的虚拟函数 `GetDecls` 中获取的。

在 `>parse` 期间创建的 C++ 项在 `>fix` 编辑代码时与代码保持同步。这包括删除被擦除代码的 C++ 项,并逐步编译插入的代码以创建新的 C++ 项。但是,函数实现的更改不会逐步编译;更简单的方法是直接删除整个实现并重新编译新的实现。

性能

我的系统大约需要 2 分 20 秒来 `>read buildlib`、`>parse`、`>check` 和 `>export` 所有 RSC 代码。这使用的是 RSC 的发布版本,它禁用了各种优化以便进行调试(它比调试版本快约 3.5 倍,但只有完全优化的发布版本的一半快)。微软的 C++ 编译器构建 RSC 花费的时间几乎完全相同。当然,这并不是真正意义上的同类比较,因为该工具不进行内存布局、不生成目标代码,也不为每个翻译单元生成文件。但它只使用一个核心,没有完全优化,收集常规编译器不收集的信息(在 `>parse` 期间),检查代码(`>check`),并生成几个大型文件(`>export`)。

RSC 包含约 840 个源代码文件和约 10 万行代码,不包括空白行或仅包含大括号或注释的行。当其发布版本在 Win32 下使用其默认配置文件进行初始化时,它使用约 100MB。执行 `>read buildlib`、`>parse`、`>check` 和 `>export` 后,它又增加了约 125MB。

这些工具不生成任何中间文件或临时文件;所有内容都保存在内存中。使用文件将是一个重大改变,因此可用内存量最终限制了这些工具可容纳的代码库的大小。但我猜测,拥有那么多代码的任何人也可以提供足够内存的机器——或者直接购买该工具的商业版本。

代码文件列表

_ct_ 目录包含所有代码。如果您想深入了解它,这里是该目录中文件的摘要:

文件 描述
CodeCoverage 代码覆盖工具(本文未讨论)
CodeDir 包含源代码的目录
CodeDirSet 一组代码目录
CodeFile 包含源代码的文件
CodeFileSet 一组代码文件
CodeItemSet 一组 C++ 代码项
CodeSet CodeDirSet、CodeFileSet 和 CodeItemSet 的基类
CodeTypes 用于解析和静态分析的类型
CtIncrement 适用于 _ct_ 目录的 CLI 命令
CtModule _ct_ 目录的初始化
Cxx C++ 类型
CxxArea 命名空间、类和类模板实例
CxxCharLiteral 字符字面量
CxxDirective 预处理器指令
CxxExecute 编译期间跟踪代码
CxxFwd 前向声明
CxxLocation 跟踪项在源文件中的位置
CxxNamed 低级命名 C++ 项
CxxRoot 全局命名空间和内置终结符
CxxScope 代码块、数据项和函数
CxxScoped 参数、基类、枚举、枚举器、前向、友元、终结符、typedef、using
CxxStatement 函数中使用的语句
CxxStrLiteral 字符串字面量
CxxString 字符串实用程序
CxxSymbols 解析器符号表
CxxToken 低级未命名 C++ 项
CxxVector C++ 项向量的函数模板
编辑器 用于 `>fix` 命令的源代码编辑器
Interpreter 解释(CLI 命令中)操作 LibrarySet 子类实例的表达式
Lexer Parser 和 Editor 的词法分析
代码文件、代码目录和 CLI 符号
LibraryErrSet 当 CLI 命令不适用于某个集合时生成错误消息
LibraryItem CodeDir、CodeFile、LibrarySet 和 CxxToken 的基类
LibrarySet CodeSet、LibraryErrSet 和 LibraryVarSet 的基类(可应用 CLI 命令的项集)
LibraryTypes 代码库的类型
LibraryVarSet 内置或用户定义的库变量
解析器 C++ 源代码解析器
SetOperations LibrarySet 实例的差集、交集和并集运算符

注释

1 在撰写本文时,我查看了是否有任何变化。Google 的项目最终获得了关注,现在在 GitHub 上。他们采用了基于 Clang 构建的方法,他们说他们目前处于“alpha”质量,并且 Clang 的更改有时会破坏他们。

2 文件 _help.cli_ 具有 _.txt_ 扩展名,本文中省略了文件名的该扩展名。

3 代码文件被假定为没有扩展名(例如,`<string>`)或带有 _.h_、_.c_、_.hpp_、_.cpp_、_ .hxx_、_ .cxx_、_ .hh_、_ .cc_、_ .h++_ 或 _ .c++ _ 扩展名的任何文件。这在 `CxxString::IsCodeFile` 中硬编码。

4 自此文撰写以来,_Duration.h_ 通过使用 `` 得到了简化,因此 _Thread.cpp_ 不再出现在其实现者列表中。

历史

  • 2019年10月4日:初始版本

自本文初版以来,已进行了多次更新。当下载内容发生变化时,本文会重新发布,这发生在新的发布版本包含错误修复和以下一项或多项内容时:

  • 增强 `>parse` 以支持更多 C++ 语言功能
  • 增强 `>check` 以生成新警告
  • 增强 `>fix` 以解决更多警告

这些更改总结在 RSC 的 发布 页面上,通常在发行说明的 `CodeTools` 部分下。

© . All rights reserved.