Dev_style

Nvim :help 页面,由 生成,从 源文件 使用 tree-sitter-vimdoc 解析器。


Nvim 风格指南
针对在 Nvim 源代码中工作的开发人员的风格指南。

背景

我们保持代码库可管理的一种方法是强制执行一致性。任何程序员能够查看另一个人的代码并快速理解它非常重要。
维护统一的风格并遵循约定意味着我们可以更容易地使用“模式匹配”来推断各种符号是什么以及关于它们的哪些不变式是正确的。创建常见的、必需的习语和模式使代码更容易理解。
在某些情况下,可能存在更改某些风格规则的充分理由,但我们仍然保持现状以保持一致性。

头文件 dev-style-header

头文件保护
所有头文件都应该以#pragma once开头以防止多次包含。
在 foo/bar.h 中
#pragma once
头文件系统
Nvim 使用两种类型的头文件。有“正常”头文件和“defs”头文件。通常,每个正常头文件都会有一个对应的 defs 头文件,例如fileio.hfileio_defs.h。这种区分是为了在更改时最小化重新编译。原因是,添加函数或修改函数的签名比更改类型更频繁。目标是实现以下内容
所有头文件(defs 和正常)只能包含 defs 头文件、系统头文件和生成的声明。换句话说,头文件不能包含正常头文件。
源文件(.c)可以包含所有头文件,但如果它们需要符号而不是类型,则应该只包含正常头文件。
使用以下指南确定将什么放在哪里
符号
常规函数声明
extern 变量(包括EXTERN 宏)
非符号
宏,即#define
带有FUNC_ATTR_ALWAYS_INLINE 属性的静态内联函数
typedefs
结构体
枚举
所有符号都必须移到正常头文件中。
多个头文件使用的非符号应移到 defs 头文件中。这是为了确保头文件只包含 defs 头文件。反之,只被单个头文件使用的非符号应该移到该头文件中。
例外:如果宏调用函数,则必须将其移到正常头文件中。

作用域 dev-style-scope

局部变量
将函数的变量放在尽可能小的作用域内,并在声明中初始化变量。
C99 允许你在函数中的任何地方声明变量。在尽可能小的作用域内声明它们,并在第一次使用之前尽可能地靠近它们。这使读者更容易找到声明,并查看变量的类型以及它被初始化为何值。特别是,应该使用初始化而不是声明和赋值,例如
int i;
i = f();      // ❌: initialization separate from declaration.
int j = g();  // ✅: declaration has initialization.
初始化
如果多个声明没有被初始化,则可以在一行中定义它们,但每个初始化都应该在单独的一行中完成。
int i;
int j;              // ✅
int i, j;           // ✅: multiple declarations, no initialization.
int i = 0;
int j = 0;          // ✅: one initialization per line.
int i = 0, j;       // ❌: multiple declarations with initialization.
int i = 0, j = 0;   // ❌: multiple declarations with initialization.

Nvim 特定的魔法

clint
使用clint.py来检测风格错误。
src/clint.py 是一个 Python 脚本,它读取源文件并识别风格错误。它并不完美,既有误报也有漏报,但它仍然是一个有价值的工具。误报可以通过在行末添加// NOLINT来忽略。
uncrustify
src/uncrustify.cfg 是对预期代码格式的权威,用于 clint.py 未涵盖的情况。如果由 uncrustify 规则涵盖,我们在 clint.py 中删除检查。

其他 C 特性 dev-style-features

可变长数组和 alloca()
我们不允许使用可变长数组或alloca()
可变长数组会导致难以检测的栈溢出。
后缀递增和后缀递减
在语句中使用后缀形式 (i++)。
for (int i = 0; i < 3; i++) { }
int j = ++i;  // ✅: ++i is used as an expression.
for (int i = 0; i < 3; ++i) { }
++i;  // ❌: ++i is used as a statement.
const 的使用
尽可能使用const 指针。避免在非指针参数定义中使用const
将 const 放在哪里
有些人更喜欢int const *foo 而不是const int *foo 的形式。他们认为这更具可读性,因为它更一致:它遵循const 始终紧跟其描述的对象的规则。然而,这种一致性论点在很少有深度嵌套的指针表达式的代码库中并不适用,因为大多数const 表达式只有一个const,并且它适用于底层值。在这种情况下,没有一致性需要保持。将const 放在前面可能更具可读性,因为它遵循英语将“形容词”(const)放在“名词”(int)前面的规则。
也就是说,虽然我们鼓励将const 放在前面,但我们并不强制要求。但是要与你周围的代码保持一致!
void foo(const char *p, int i);
}
int foo(const int a, const bool b) {
}
int foo(int *const p) {
}
整数类型
在内置整数类型中,只使用charintuint8_tint8_tuint16_tint16_tuint32_tint32_tuint64_tint64_tuintmax_tintmax_tsize_tssize_tuintptr_tintptr_tptrdiff_t
仅对错误代码和局部、简单的变量使用int
转换整数类型时要小心。整数转换和提升会导致非直观的行为。请注意,char 的有符号性是实现定义的。
面向公众的类型必须具有固定宽度 (uint8_t 等)
对于固定宽度类型,没有方便的printf 格式占位符。如果你必须格式化固定宽度整数,则将其强制转换为uintmax_tintmax_t
类型 无符号 有符号 char %hhu %hhd int n/a %d (u)intmax_t %ju %jd (s)size_t %zu %zd ptrdiff_t %tu %td
布尔值
使用bool 来表示布尔值。
int loaded = 1;  // ❌: loaded should have type bool.
条件
不要使用“Yoda 条件”。每个条件中最多使用一个赋值。
if (1 == x) {
if (x == 1) {  //use this order
if ((x = f()) && (y = g())) {
函数声明
每个函数都不应该有单独的声明。
函数声明是由 gen_declarations.lua 脚本创建的。
static void f(void);
static void f(void)
{
  ...
}
通用翻译单元布局
公有函数的定义位于静态函数的定义之前。
<HEADER>
<PUBLIC FUNCTION DEFINITIONS>
<STATIC FUNCTION DEFINITIONS>
与声明生成器的集成
每个 C 文件都必须包含生成的头的 #include,由 #ifdef INCLUDE_GENERATED_DECLARATIONS 保护。
包含必须在 .c 文件中的其他 #include 和 typedefs 之后,以及在头文件中的所有其他内容之后。如果 .c 文件不包含任何静态函数,则允许省略 #include。
包含的文件名由 .c 文件名(不含扩展名)组成,前面是相对于 src/nvim 的目录名。包含静态函数声明的文件名以 .c.generated.h 结尾,*.h.generated.h 文件只包含非静态函数声明。
// src/nvim/foo.c file
#include <stddef.h>
typedef int FooType;
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "foo.c.generated.h"
#endif
…
// src/nvim/foo.h file
#pragma once
…
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "foo.h.generated.h"
#endif
64 位可移植性
代码应该与 64 位和 32 位兼容。请牢记打印、比较和结构对齐的问题。
请记住,sizeof(void *) != sizeof(int)。如果你想要一个指针大小的整数,请使用intptr_t
你可能需要小心结构对齐,特别是对于存储在磁盘上的结构。任何包含int64_t/`uint64_t` 成员的类/结构默认情况下将在 64 位系统上进行 8 字节对齐。如果你有这样的结构在 32 位和 64 位代码之间在磁盘上共享,你需要确保它们在两种架构上的打包方式相同。大多数编译器都提供了一种修改结构对齐的方式。对于 gcc,你可以使用__attribute__((packed))。MSVC 提供#pragma pack()__declspec(align())
根据需要使用LLULL 后缀来创建 64 位常量。例如
int64_t my_value = 0x123456789LL;
uint64_t my_mask = 3ULL << 48;
sizeof ~
优先使用sizeof(varname) 而不是sizeof(type)
当你获取特定变量的大小的时候,使用sizeof(varname)。如果有人现在或以后更改变量类型,sizeof(varname) 会适当地更新。对于与任何特定变量无关的代码,例如管理外部或内部数据格式的代码(其中合适的 C 类型变量不方便),你可以使用sizeof(type)
Struct data;
memset(&data, 0, sizeof(data));
memset(&data, 0, sizeof(Struct));
if (raw_size < sizeof(int)) {
  fprintf(stderr, "compressed record not big enough for count: %ju", raw_size);
  return false;
}

命名 dev-style-naming

最重要的保持一致性规则是那些管理命名的规则。一个名称的风格立即告诉我们命名实体是什么类型:类型、变量、函数、常量、宏等,而无需我们去查找该实体的声明。我们大脑中的模式匹配引擎很大程度上依赖于这些命名规则。
命名规则非常随意,但我们认为在这个领域,一致性比个人偏好更重要,所以无论你是否觉得它们合理,规则就是规则。
一般命名规则
函数名、变量名和文件名应该具有描述性;避免使用缩写。
在合理的范围内,给出尽可能具有描述性的名称。不要担心节省水平空间,因为让你的代码对新读者立即可理解要重要得多。不要使用对项目外部的读者来说模棱两可或不熟悉的缩写,也不要通过删除单词中的字母来缩写。
int price_count_reader;    // No abbreviation.
int num_errors;            // "num" is a widespread convention.
int num_dns_connections;   // Most people know what "DNS" stands for.
int n;                     // Meaningless.
int nerr;                  // Ambiguous abbreviation.
int n_comp_conns;          // Ambiguous abbreviation.
int wgc_connections;       // Only your group knows what this stands for.
int pc_reader;             // Lots of things can be abbreviated "pc".
int cstmr_id;              // Deletes internal letters.
文件名
文件名应该全部小写,可以包含下划线 (_)。
使用下划线来分隔单词。可接受的文件名示例
my_useful_file.c
getline_fix.c  // ✅: getline refers to the glibc function.
C 文件应该以.c 结尾,头文件应该以.h 结尾。
不要使用/usr/include 中已存在的文件名,例如db.h
一般来说,让你的文件名非常具体。例如,使用http_server_logs.h 而不是logs.h
类型名称
使用 typedef 的结构体和枚举以大写字母开头,每个新单词都以大写字母开头,没有下划线:MyExcitingStruct
不使用 typedef 的结构体和枚举全部小写,单词之间用下划线分隔:struct my_exciting_struct
struct my_struct {
  ...
};
typedef struct my_struct MyAwesomeStruct;
变量名称
变量名称全部小写,单词之间用下划线分隔。例如:my_exciting_local_variable
常用变量名称
例如
string table_name;  // ✅: uses underscore.
string tablename;   // ✅: all lowercase.
string tableName;   // ❌: mixed case.
结构体变量
结构体中的数据成员应该像普通变量一样命名。
struct url_table_properties {
  string name;
  int num_entries;
}
全局变量
除非绝对必要,否则不要使用全局变量。全局变量前缀为g_
常量名称
使用k 后跟混合大小写:kDaysInAWeek
所有编译时常量,无论是在本地还是全局声明,都遵循与其他变量略有不同的命名约定。使用k 后跟第一个字母为大写的单词
const int kDaysInAWeek = 7;
函数名称
函数名称全部小写,单词之间用下划线分隔。例如:my_exceptional_function()。同一个头文件中的所有函数都应该有一个共同的前缀。
os_unix.h
void unix_open(const char *path);
void unix_user_id(void);
如果你的函数在出错时崩溃,你应该在函数名称后附加or_die。这仅适用于可能被生产代码使用的函数,以及在正常操作期间可能发生的错误。
枚举器名称
枚举器应该像常量一样命名:kEnumName
enum url_table_errors {
  kOK = 0,
  kErrorOutOfMemory,
  kErrorMalformedInput,
};
宏名称
它们是这样的:MY_MACRO_THAT_SCARES_CPP_DEVELOPERS
#define ROUND(x) ...
#define PI_ROUNDED 5.0

注释 dev-style-comments

注释对于保持代码可读性至关重要。以下规则描述了您应该注释的内容以及注释的位置。但请记住:虽然注释非常重要,但最好的代码是自文档化的。
编写注释时,要为您的受众而写:下一个需要理解您的代码的贡献者。要慷慨一些——下一个可能就是您自己!
Nvim 使用 Doxygen 注释。
注释风格
只使用 // 风格的语法。
// This is a comment spanning
// multiple lines
f();
文件注释
每个文件开头都应该有一个描述其内容的描述。
法律声明
我们没有这样的东西。这些东西都在 LICENSE 中,只在那里。
文件内容
每个文件顶部都应该有一个注释,描述其内容。
通常,.h 文件将描述文件中声明的变量和函数,概述它们的用途以及如何使用它们。.c 文件应该包含有关实现细节或对棘手算法的讨论的更多信息。如果您认为实现细节或对算法的讨论对于阅读 .h 的人来说会很有用,请随时将其放在那里,但要在 .c 中提到文档在 .h 文件中。
不要在 .h.c 中都重复注释。重复的注释会产生分歧。
/// A brief description of this file.
///
/// A longer description of this file.
/// Be very generous here.
结构体注释
每个结构体定义都应该有配套的注释,描述其用途以及如何使用它。
/// Window info stored with a buffer.
///
/// Two types of info are kept for a buffer which are associated with a
/// specific window:
/// 1. Each window can have a different line number associated with a
/// buffer.
/// 2. The window-local options for a buffer work in a similar way.
/// The window-info is kept in a list at g_wininfo.  It is kept in
/// most-recently-used order.
struct win_info {
  /// Next entry or NULL for last entry.
  WinInfo *wi_next;
  /// Previous entry or NULL for first entry.
  WinInfo *wi_prev;
  /// Pointer to window that did the wi_fpos.
  Win *wi_win;
  ...
};
如果字段注释很短,您也可以将它们放在字段旁边。但在同一个结构体中保持一致,并遵循必要的 Doxygen 风格。
struct wininfo_S {
  WinInfo *wi_next;  ///< Next entry or NULL for last entry.
  WinInfo *wi_prev;  ///< Previous entry or NULL for first entry.
  Win *wi_win;       ///< Pointer to window that did the wi_fpos.
  ...
};
如果您已经在文件顶部的注释中详细描述了一个结构体,可以随意简单地说明“参见文件顶部的注释以获取完整描述”,但请确保有一些注释。
记录结构体所做的同步假设(如果有)。如果结构体的实例可以被多个线程访问,请格外小心地记录多线程使用周围的规则和不变式。
函数注释
声明注释描述了函数的使用;函数定义处的注释描述了操作。
函数声明
每个函数声明都应该有紧接其前的注释,描述该函数的作用以及如何使用它。这些注释应该是描述性的(“打开文件”)而不是命令式的(“打开文件”);注释描述了函数,而不是告诉函数该做什么。通常,这些注释不描述函数如何执行其任务。相反,这应该留给函数定义中的注释。
函数声明注释中要提到的内容类型
如果函数分配了调用者必须释放的内存。
任何参数是否可以为空指针。
如果函数的用法有任何性能影响。
如果函数是可重入的。它的同步假设是什么?
/// Brief description of the function.
///
/// Detailed description.
/// May span multiple paragraphs.
///
/// @param arg1 Description of arg1
/// @param arg2 Description of arg2. May span
///        multiple lines.
///
/// @return Description of the return value.
Iterator *get_iterator(void *arg1, void *arg2);
函数定义
如果函数完成工作的方式有任何棘手之处,函数定义应该有一个解释性注释。例如,在定义注释中,您可以描述您使用的任何编码技巧,概述您所执行的步骤,或解释为什么您选择以您所做的方式实现函数,而不是使用可行的替代方法。例如,您可以提及为什么它必须为函数的前半部分获取锁,但为什么对于后半部分不需要获取锁。
请注意,您不应该只是重复 .h 文件或其他地方给出的函数声明中的注释。可以简要地概括一下函数的功能,但注释的重点应该是它是如何工作的。
// Note that we don't use Doxygen comments here.
Iterator *get_iterator(void *arg1, void *arg2)
{
  ...
}
变量注释
通常,变量的实际名称应该足够描述性,以便对变量的使用方式有一个很好的了解。在某些情况下,需要更多注释。
全局变量
所有全局变量都应该有一个注释,描述它们是什么以及它们用于什么。例如
/// The total number of tests cases that we run
/// through in this regression test.
const int kNumTestCases = 6;
实现注释
在您的实现中,您应该在代码中棘手、不明显、有趣或重要的部分添加注释。
行注释
此外,不明显的行应该在行末添加注释。这些行末注释应该用 2 个空格与代码隔开。示例
// If we have enough memory, mmap the data portion too.
mmap_budget = max<int64>(0, mmap_budget - index_->length());
if (mmap_budget >= data_size_ && !MmapData(mmap_chunk_bytes, mlock)) {
  return;  // Error already logged.
}
请注意,既有描述代码在做什么的注释,也有提到函数返回时已经记录错误的注释。
如果您在后续行上有多个注释,通常将它们对齐可以更具可读性
do_something();                      // Comment here so the comments line up.
do_something_else_that_is_longer();  // Comment here so there are two spaces between
                                     // the code and the comment.
{ // One space before comment when opening a new scope is allowed,
  // thus the comment lines up with the following comments and code.
  do_something_else();  // Two spaces before line comments normally.
}
NULL、true/false、1、2、3...
当您将空指针、布尔值或文字整数值传递给函数时,您应该考虑添加关于它们是什么的注释,或者通过使用常量使您的代码自文档化。例如,比较
bool success = calculate_something(interesting_value,
                                   10,
                                   false,
                                   NULL);  // What are these arguments??
bool success = calculate_something(interesting_value,
                                   10,     // Default base value.
                                   false,  // Not the first time we're calling this.
                                   NULL);  // No callback.
或者,常量或自描述变量
const int kDefaultBaseValue = 10;
const bool kFirstTimeCalling = false;
Callback *null_callback = NULL;
bool success = calculate_something(interesting_value,
                                   kDefaultBaseValue,
                                   kFirstTimeCalling,
                                   null_callback);
不要
请注意,您不应该描述代码本身。假设阅读代码的人比您更了解 C,即使他或她不知道您要做什么
// Now go through the b array and make sure that if i occurs,
// the next element is i+1.
...        // Geez.  What a useless comment.
标点符号、拼写和语法
注意标点符号、拼写和语法;阅读写得好的注释比阅读写得不好的注释更容易。
注释应该像叙述性文本一样易读,具有适当的字母大小写和标点符号。在许多情况下,完整的句子比句子片段更易读。较短的注释,例如代码行末的注释,有时可以不那么正式,但您应该保持一致的风格。
虽然代码审查者指出您使用逗号时应该使用分号可能会令人沮丧,但源代码必须保持高度清晰和可读性非常重要。正确的标点符号、拼写和语法有助于实现这一目标。
TODO 注释
对于临时代码、短期解决方案或足够好但并不完美,请使用 TODO 注释。
TODO 应该包含全大写的字符串 TODO,后面跟着可以最好地提供 TODO 所引用的问题上下文的名称、电子邮件地址或其他标识符。主要目的是有一个一致的 TODO 格式,可以搜索它以找到可以应要求提供更多详细信息的人员。TODO 不是对所引用的问题进行修复的承诺。因此,当您创建 TODO 时,它几乎总是您自己的姓名。
// TODO([email protected]): Use a "*" here for concatenation operator.
// TODO(Zeke): change this to use relations.
如果您的 TODO 是“将来某个日期做某事”的形式,请确保您要么包含一个非常具体的日期(“在 2005 年 11 月之前修复”)或一个非常具体的事件(“当所有客户端都可以处理 XML 响应时删除此代码”。)。
弃用注释
使用 @deprecated 文档字符串标记来标记已弃用的接口点。
您可以通过编写包含全大写单词 @deprecated 的注释来将接口标记为已弃用。注释放在接口声明之前或与声明位于同一行。
@deprecated 后,在括号中写下您的姓名、电子邮件或其他标识符。
弃用注释必须包含简单明了的说明,供人们修复其调用点。在 C 中,您可以将已弃用的函数实现为一个内联函数,该函数调用新的接口点。
将接口点标记为 DEPRECATED 不会神奇地导致任何调用点发生更改。如果您希望人们真正停止使用已弃用的工具,您将不得不自己修复调用点或招募一批人员来帮助您。
新代码不应包含对已弃用接口点的调用。改用新的接口点。如果您不理解说明,请找到创建弃用的人并向他们寻求使用新接口点的帮助。

格式化 dev-style-format

编码风格和格式是相当随意的,但如果每个人都使用相同的风格,项目就会更容易遵循。个人可能不同意格式化规则的每个方面,某些规则可能需要一些时间才能习惯,但重要的是所有项目贡献者都遵循风格规则,以便他们都可以轻松地阅读和理解每个人的代码。
非 ASCII 字符
非 ASCII 字符应该很少见,并且必须使用 UTF-8 格式。
您不应该在源代码中硬编码面向用户的文本(或者您应该吗?),即使是英语,因此非 ASCII 字符的使用也应该很少见。但是,在某些情况下,在您的代码中包含此类单词是合适的。例如,如果您的代码解析来自国外来源的数据文件,则可能适合将这些数据文件中用作分隔符的非 ASCII 字符串硬编码。更常见的是,单元测试代码(不需要本地化)可能包含非 ASCII 字符串。在这种情况下,您应该使用 UTF-8,因为 UTF-8 是一种由大多数能够处理 ASCII 以外内容的工具理解的编码。
十六进制编码也可以,并且鼓励在提高可读性的地方使用——例如,"\uFEFF" 是 Unicode 零宽度不间断空格字符,如果在源代码中包含为直接 UTF-8,则将不可见。
花括号初始化列表
以与您在函数调用中格式化相同的方式格式化花括号列表,但 { 后面有一个空格,} 前面有一个空格
如果花括号列表紧跟在名称后面(例如类型或变量名称),则像函数调用带有该名称的括号一样进行格式化。如果没有名称,则假定为零长度名称。
struct my_struct m = {  // Here, you could also break before {.
    superlongvariablename1,
    superlongvariablename2,
    { short, interior, list },
    { interiorwrappinglist,
      interiorwrappinglist2 } };
循环和 Switch 语句
注释非平凡的情况之间的贯穿。
如果不以枚举值作为条件,switch 语句应该始终有 default case(如果是枚举值,如果未处理任何值,编译器会向您发出警告)。如果 default case 不应该执行,只需使用 abort()
switch (var) {
  case 0:
    ...
    break;
  case 1:
    ...
    break;
  default:
    abort();
}
以枚举值为条件的 Switch 语句,如果它是详尽无遗的,则不应该有 default case。明确的 case 标签优先于 default,即使它会导致同一代码有多个 case 标签。例如,而不是
case A:
  ...
case B:
  ...
case C:
  ...
default:
  ...
您应该使用
case A:
  ...
case B:
  ...
case C:
  ...
case D:
case E:
case F:
  ...
某些编译器不识别详尽的枚举 switch 语句为详尽无遗,这会导致在 switch 语句的每个 case 中都有 return 语句,但没有 catch-all return 语句时出现编译器警告。为了修复这些虚假错误,建议您在 switch 语句之后使用 UNREACHABLE 来明确告诉编译器 switch 语句始终返回,并且任何之后的代码都是不可到达的。例如
enum { A, B, C } var;
...
switch (var) {
  case A:
    return 1;
  case B:
    return 2;
  case C:
    return 3;
}
UNREACHABLE;
返回值
不要不必要地用括号将 return 表达式括起来。
return expr 中使用括号;仅在您会在 inx = expr; 中使用它们的地方使用。
return result;
return (some_long_condition && another_condition);
return (value);  // You wouldn't write var = (value);
return(result);  // return is not a function!
水平空格
水平空格的使用取决于位置。
变量
int long_variable = 0;  // Don't align assignments.
int i             = 1;
struct my_struct {  // Exception: struct arrays.
  const char *boy;
  const char *girl;
  int pos;
} my_variable[] = {
  { "Mia",       "Michael", 8  },
  { "Elizabeth", "Aiden",   10 },
  { "Emma",      "Mason",   2  },
};

结语

本风格指南旨在使代码更具可读性。如果您认为必须为了清晰起见而违反其规则,请这样做!但是,请在您的拉取请求中添加一条说明您理由的注释。
命令索引
快速参考