分析
概述
clice analyze 读取持久化索引并报告代码中的事实,供编程智能体据此作出重构决策。它不执行构建或计时,只描述代码;编程智能体则结合手头的代码决定如何修改。与 clice query 一样,它读取 clice index 构建的索引,并将 JSON 输出到标准输出。
用法:clice analyze <analysis> [--workspace <dir>] [--configuration <tag>] [options]
模块
clice analyze modules 描述程序各目录之间的依赖关系,为将程序划分为 C++20 具名模块提供依据:哪些循环依赖阻碍了划分、每个循环依赖由哪几个关键实体造成、哪些头文件应归入别处,以及哪些代码结构无法通过机械改写迁移到模块中。
分析范围内的每个文件都属于一个模块,默认按所在目录划分。当一个模块中的文件通过名称引用另一个模块的文件所提供的实体时,前者就依赖于后者:既包括直接写出的名称,也包括在该文件中由宏展开产生的名称,以及显式特化所针对的模板。提供实体的文件优先取定义该实体的头文件,其次是声明该实体的头文件,最后是定义该实体的源文件。来自头文件的依赖是接口边,会转化为对所依赖模块接口的导入;仅来自源文件的依赖是实现边,不会形成循环依赖,因为实现单元可以导入位于其上层的模块。.inc 和 .def 等文本片段文件视为包含它们的文件的一部分。如果一个头文件仅供其所属模块的源文件使用,无论是直接使用,还是通过其他此类头文件间接使用,它都属于内部分区:它不会向模块接口添加任何内容,其依赖也都不是接口边。
--scope <glob,...>仅分析与 glob 模式匹配的文件,匹配时使用相对于工作区的路径;默认分析工作区下所有已建立索引且不位于名称以点开头的目录中的文件。--depth <n>使用文件所在目录路径的前n段命名其默认模块,而不是使用完整目录路径。--partition <file>按 glob 模式分配模块,以首次匹配为准:{"modules": [{"name": "core", "files": ["src/support/**", "src/vfs/**"]}]}。未匹配任何 glob 模式的文件仍归入其所在目录对应的模块。对于interface视图和 modularize,"textual": true让模块保持为头文件,"external": true标记由已有模块代替的模块(即标准库),"provides": "std.compat"指定导出该模块中名称的模块,这些名称从--std <dir>指定的 libc++ 模块源码中读取。--move <path>=<module>,...和--merge <module>+<module>[+...],...评估假设的变更,但不实际执行变更。--move-entity <name>=<header>,...评估将声明连同其成员及其定义所引用的实体移至另一个头文件(已有或新建)的效果;可使用edge视图中的#<id>从多个重载中选定一个。--annotation <file>,...和--churn-since <date>对结果加权;参见注解。--view <view>选择结果视图,默认为overview。
| 视图 | 提供的信息 |
|---|---|
overview | 按层划分的模块、附有实体示例的边、循环依赖及打破各循环依赖所需移除的最轻边集、模块内的头文件循环依赖、内部分区数量、最深导入链、重新构建总数、热点、移动候选和拆分候选,以及障碍数量 |
edge --from <module> --to <module> | 该边承载的所有实体,以及各实体的 id、所有者和每处使用位置的 file:line |
module --module <module> | 按模块列出的各文件依赖项及依赖方、该模块的内部分区,以及按使用它们的模块分组的其他头文件——这些就是目录拆分的边界 |
file --file <path> | 该文件按模块列出所引用的实体、哪些文件引用它的实体,以及它的注解 |
obstacles | 下文列出的所有代码结构 |
macros | 按模块列出在其定义文件之外使用的宏:导入不传递宏,因此每个宏都通过文本头文件传递给使用方 |
impact | 对于每个头文件,修改它后在当前结构下和采用分区方案后分别需要重新构建的单元,以及该头文件是否为内部分区 |
interface [--module <module>] | 对于每个模块,为其头文件编写封装模块所需的信息:它导入的模块、它的入口头文件及其他模块包含这些头文件时所用的名称、它在命名空间作用域声明的名称及所在头文件、它的命名空间别名、仍以文本方式包含的头文件、它的头文件与其他封装模块共用的 TU 内部实体、导入方所需的宏,以及它的头文件读取的其他模块的宏 |
概览中的热点、移动候选和拆分候选条目数受 --limit 限制(默认为 20);模块、边和循环依赖总是完整列出。
移动候选是这样的头文件:其使用方全部位于同一个其他模块中;它与实现它的源文件,以及这些源文件实现的其他头文件组成一组,这组文件既不引用所属模块中的其他文件,也不被其中的其他文件引用,且不涵盖整个模块。这些文件会一并列出,以便一起移动。拆分候选是这样的头文件:其实体可以分成若干组,各组的使用模块互不重叠;每组按行号列出前十个实体,格式为 name:line,并用 count 给出实体总数。
障碍
- 索引中存在多个记录变体的头文件,其解析结果会因包含它的文件而异;作为模块接口编译一次后,它只会保留其中一种解析结果。
declarationsDiffer标记各变体声明了不同实体的情况,这表明存在实际的配置依赖,概览只统计这类情况;unstable列出仅被部分变体引用的实体,例如模板中的依赖调用(dependent call)在包含该头文件的位置找到的重载。 - 如果头文件在预处理条件中检测某个宏,却未包含该宏的定义,单独编译时的解析结果就会悄然改变(
contextMacros);如果头文件展开这样的宏,则必须先包含宏定义才能单独编译通过(borrowedMacros)。 - 如果头文件中的
static实体或匿名命名空间实体被其他文件或该头文件自身的内联代码使用,那么一旦该头文件成为模块接口,这些实体就会局限于一个翻译单元内。 - 在模块单元中,对其他模块提供的实体所作的前置声明或友元声明,实际声明的是另一个实体;对第三方实体作这样的声明,则会将其附着到错误的模块。
unused标记所声明实体从未被该文件引用的这类声明,可以直接删除。 - 在一个模块的头文件中声明、却在另一个模块的源文件中定义的实体,以及在多个头文件中定义的实体。
- 如果头文件定义的宏会被第三方头文件读取(
configuringMacros),该宏的定义必须保留在全局模块片段中,并位于包含该第三方头文件的指令之前。 - 如果头文件针对并非由自身提供的实参对第三方模板进行特化(
implicitProviders),每次实例化都会用到该头文件,却不会显式引用它,因此没有依赖边记录这些使用方;该头文件需要留在所有进行实例化的单元都能导入它的位置。 - 也会列出对其他模块中模板的显式特化:这些特化是合法的,但只有在导入特化所属模块的地方才可达。
重新构建估算
修改模块接口后,所有直接导入它或通过其他接口间接导入它的模块,其全部单元都需要重新编译:编译后的模块接口会记录它导入的每个接口的哈希值。impact 和概览中的总数以翻译单元为单位,将这一数量与当前修改该头文件会重新构建的单元数量进行比较。修改内部分区只会重新构建依赖该分区的源文件;无论使用头文件还是模块,修改源文件都只会重新构建一个单元。这些估算偏保守,只统计程序自身的源文件。
注解
注解为每个文件提供一个带名称的数值,用于对结果加权,就像优化器依据性能分析数据权衡各种选择一样;即使没有任何注解,各项结果仍然有意义。
churn表示文件的变更频率:默认根据git log,统计过去六个月内修改过该文件的提交次数(--churn-since <date>接受git log --since支持的任意值;传入空值则不计算该注解)。热点和重新构建总数都会乘以该值。compile_time表示重新构建一个翻译单元的耗时。未提供时,每个翻译单元都按 1 计。
注解文件的格式为 {"name": "compile_time", "unit": "s", "values": {"src/main.cpp": 4.2}},其中的路径相对于工作区;若注解名称与某个计算生成的注解相同,则会替换后者。
回答
每个回答都是一个 JSON 对象;如果无法回答问题(没有索引、未知的模块或文件、无效的 glob 模式),则返回 {"error": "...", "stale": [...]},退出码为 1;参数无法解析时同样如此,退出码为 2;索引缺少部分单元时同样失败,并在 stale 中列出这些单元。
