@chengzhao-dev/dsh-cpp-workflow 0.6.6 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * 注释标签策略:设置页的预览、技能快照与检查器参数共用这一份。
3
3
  *
4
- * 配置只描述「写注释时默认带哪些标签」,不描述写法本身;写法在
5
- * `skills/developing-cpp/references/comments.md`。设置页保存的值由宿主
4
+ * 配置只描述「写注释时默认带哪些标签、声明用哪种注释符号」,不描述写法本身;
5
+ * 写法在 `skills/developing-cpp/references/comments.md`。设置页保存的值由宿主
6
6
  * 写回插件 Config,服务端 `apply(ctx, config)` 用它生成本页快照与检查器参数。
7
7
  *
8
8
  * 这份文件被 `tsc` 编进 `lib/comment-policy.js`,打包客户端半侧时再内联进
@@ -10,29 +10,16 @@
10
10
  */
11
11
  /** 文件头标签。 */
12
12
  export const HEADER_TAGS = ['brief', 'author', 'date', 'copyright', 'version'];
13
- /** 函数契约标签:开关决定是否要求写出。 */
13
+ /** 函数声明标签:开关决定是否要求写出。 */
14
14
  export const CONTRACT_TAGS = ['param', 'returns', 'throws'];
15
- /** 折叠在「更多标签」里的可选标签:默认关闭。 */
15
+ /** 函数声明里默认关闭的补充标签。 */
16
+ export const FUNCTION_EXTRA_TAGS = ['pre', 'post', 'retval'];
17
+ /** 类型标签:默认关闭,函数模板共用同一组键。 */
18
+ export const TYPE_TAGS = ['tparam', 'note', 'see'];
19
+ /** 全部可选标签,快照与测试按它排序。 */
16
20
  export const EXTRA_TAGS = ['note', 'see', 'pre', 'post', 'tparam', 'retval'];
17
21
  /** 只有用户主动填了非空值才放行的隐私标签。 */
18
22
  export const PRIVACY_TAGS = ['author', 'date', 'copyright', 'version'];
19
- /** 标签说明:设置页逐行显示,用作行的辅助文字。 */
20
- export const TAG_TEXT = {
21
- brief: '一句话说明这个文件定义或实现什么',
22
- author: '只在用户填了非空值时写出',
23
- date: '只在用户填了非空值时写出',
24
- copyright: '只在用户填了非空值时写出',
25
- version: '只在用户填了非空值时写出',
26
- param: '函数参数的含义、范围或所有权',
27
- returns: '返回值的条件;返回类型自明时可以省略',
28
- throws: '抛出的异常类型与条件;不抛异常的函数不写',
29
- note: '调用方需要注意的补充说明',
30
- see: '相关函数、类型或文档',
31
- pre: '调用前必须成立的条件',
32
- post: '调用后保证成立的条件',
33
- tparam: '模板参数',
34
- retval: '特定返回值的含义',
35
- };
36
23
  /** 开关型标签在设置页的标题。 */
37
24
  export const TAG_LABEL = {
38
25
  file: '@file',
@@ -55,8 +42,9 @@ export const TAG_LABEL = {
55
42
  export const SAMPLE_BRIEF = '<一句话说明本文件>';
56
43
  /** 预览里的示例文件名:只是占位符,不对应任何工程的真实文件。 */
57
44
  export const SAMPLE_FILE = '<文件名>';
58
- /** 出厂配置:文件头只留 @file 与 @brief,契约标签全开,隐私标签全关。 */
45
+ /** 出厂配置:声明用 `///`,文件头只留 @file 与 @brief,契约标签全开,其余关闭。 */
59
46
  export const DEFAULT_CONFIG = {
47
+ commentStyle: 'slash',
60
48
  brief: { enabled: true, value: '' },
61
49
  author: { enabled: false, value: '' },
62
50
  date: { enabled: false, value: '' },
@@ -92,12 +80,16 @@ function flag(value, fallback) {
92
80
  const plain = unwrap(value);
93
81
  return typeof plain === 'boolean' ? plain : fallback;
94
82
  }
83
+ /** 声明符号只认两档;未知值回到 `///`。 */
84
+ export function commentStyleOf(raw) {
85
+ return unwrap(raw) === 'block' ? 'block' : 'slash';
86
+ }
95
87
  /**
96
88
  * 把宿主给的值补齐成完整配置;缺字段、旧版本或手工改坏的配置都回到出厂值。
97
89
  */
98
90
  export function normalizeConfig(raw) {
99
91
  const source = raw && typeof raw === 'object' ? raw : {};
100
- const config = {};
92
+ const config = { commentStyle: commentStyleOf(source.commentStyle) };
101
93
  for (const tag of HEADER_TAGS) {
102
94
  const entry = source[tag] && typeof source[tag] === 'object' ? source[tag] : {};
103
95
  config[tag] = {
@@ -110,59 +102,85 @@ export function normalizeConfig(raw) {
110
102
  }
111
103
  return config;
112
104
  }
105
+ /** 一行声明注释:`/// 内容` 或块注释里的 ` * 内容`。 */
106
+ function declarationLine(style, content) {
107
+ return style === 'block' ? ` * ${content}` : `/// ${content}`;
108
+ }
109
+ /** 文件头后面的说明行:块注释里空行写成 ` *`。 */
110
+ function separatorLine(style) {
111
+ return style === 'block' ? ' *' : '///';
112
+ }
113
113
  /**
114
- * 按配置渲染文件头;`@file` 始终是传入的文件名。
114
+ * 按配置渲染文件头;`@file` 始终是传入的文件名,块注释按 Doxygen 结构分行。
115
115
  */
116
116
  export function renderFileHeader(raw, fileName) {
117
117
  const config = normalizeConfig(raw);
118
118
  const name = text(fileName) || SAMPLE_FILE;
119
- const lines = [`/// @file ${name}`];
119
+ const lines = [`@file ${name}`];
120
120
  if (config.brief.enabled) {
121
- lines.push(config.brief.value ? `/// @brief ${config.brief.value}` : '/// @brief');
121
+ lines.push(config.brief.value ? `@brief ${config.brief.value}` : '@brief');
122
122
  }
123
123
  for (const tag of ['author', 'date', 'copyright', 'version']) {
124
124
  if (config[tag].enabled && config[tag].value)
125
- lines.push(`/// @${tag} ${config[tag].value}`);
125
+ lines.push(`@${tag} ${config[tag].value}`);
126
+ }
127
+ if (config.commentStyle === 'block') {
128
+ return ['/**', ...lines.map((line) => ` * ${line}`), ' */'];
126
129
  }
127
- return lines;
130
+ return lines.map((line) => `/// ${line}`);
128
131
  }
129
132
  /**
130
133
  * 设置页的预览:文件头 + 一段函数注释,只显示当前配置会写出的标签。
134
+ * 返回值里的 `header` 已按声明符号排好,`contract` 是每行标签的正文。
131
135
  */
132
136
  export function previewParts(raw, sampleName) {
133
137
  const config = normalizeConfig(raw);
134
138
  const header = renderFileHeader(config, sampleName || SAMPLE_FILE);
135
139
  // 预览是示例:@brief 开着但用户没填句子时,用示例文件自己的摘要顶替。
136
- const briefIndex = header.indexOf('/// @brief');
137
- if (briefIndex !== -1)
138
- header[briefIndex] = `/// @brief ${SAMPLE_BRIEF}`;
140
+ const briefIndex = header.findIndex((line) => /@brief$/.test(line.trim()));
141
+ if (briefIndex !== -1) {
142
+ header[briefIndex] = header[briefIndex].replace(/@brief$/, `@brief ${SAMPLE_BRIEF}`);
143
+ }
139
144
  const contract = [];
140
145
  if (config.param)
141
- contract.push('/// @param row 目标行号。');
146
+ contract.push('@param row 目标行号。');
142
147
  if (config.returns)
143
- contract.push('/// @return 放置成功时为 true。');
148
+ contract.push('@return 放置成功时为 true。');
144
149
  if (config.throws)
145
- contract.push('/// @throws std::out_of_range 行号越界时抛出。');
146
- const declaration = '[[nodiscard]] bool Place(int row, int column);';
147
- return { header, contract, declaration };
150
+ contract.push('@throws std::out_of_range 行号越界时抛出。');
151
+ return { header, contract, declaration: '[[nodiscard]] bool Place(int row, int column);' };
148
152
  }
149
153
  /**
150
154
  * 设置页预览的纯文本形式;客户端半侧用同一份实现渲染 `<pre>`。
155
+ * 块注释的每个内容行前加 ` * `,首尾各占一行。
151
156
  */
152
157
  export function previewText(raw, sampleName) {
153
- const { header, contract, declaration } = previewParts(raw, sampleName);
154
- const lines = [...header, '///', '/// 放置成功时返回 true;行号越界或目标格子已占时返回 false。'];
158
+ const config = normalizeConfig(raw);
159
+ const { header, contract, declaration } = previewParts(config, sampleName);
160
+ const style = config.commentStyle;
161
+ const summary = '放置成功时返回 true;行号越界或目标格子已占时返回 false。';
162
+ if (style === 'block') {
163
+ const body = [declarationLine(style, summary)];
164
+ if (contract.length > 0) {
165
+ body.push(separatorLine(style), ...contract.map((line) => declarationLine(style, line)));
166
+ }
167
+ return [...header, '/**', ...body, ' */', declaration].join('\n');
168
+ }
169
+ const lines = [...header, separatorLine(style), declarationLine(style, summary)];
155
170
  if (contract.length > 0)
156
- lines.push('///', ...contract);
171
+ lines.push(separatorLine(style), ...contract.map((line) => declarationLine(style, line)));
157
172
  lines.push(declaration);
158
173
  return lines.join('\n');
159
174
  }
160
175
  /**
161
- * 检查器参数:隐私标签要显式放行,@brief 被关掉时跳过摘要检查。
176
+ * 检查器参数:隐私标签要显式放行,@brief 被关掉时跳过摘要检查,
177
+ * 声明符号切到块注释时让检查器按块注释收口。
162
178
  */
163
179
  export function checkerArgs(raw) {
164
180
  const config = normalizeConfig(raw);
165
181
  const args = [];
182
+ if (config.commentStyle === 'block')
183
+ args.push('--comment-style', 'block');
166
184
  if (!config.brief.enabled)
167
185
  args.push('--no-brief');
168
186
  for (const tag of PRIVACY_TAGS) {
@@ -173,10 +191,11 @@ export function checkerArgs(raw) {
173
191
  }
174
192
  /**
175
193
  * 技能快照:`get()` 把它接在技能正文末尾,让代理看到当前 profile 的标签开关。
194
+ * 只写一行,列出声明符号、开启与关闭的标签,避免正文膨胀。
176
195
  */
177
196
  export function snapshotText(raw) {
178
197
  const config = normalizeConfig(raw);
179
- const on = [];
198
+ const on = ['@file'];
180
199
  const off = [];
181
200
  for (const tag of HEADER_TAGS) {
182
201
  const entry = config[tag];
@@ -186,15 +205,13 @@ export function snapshotText(raw) {
186
205
  else
187
206
  off.push(TAG_LABEL[tag]);
188
207
  }
189
- const contract = CONTRACT_TAGS.filter((tag) => config[tag]).map((tag) => TAG_LABEL[tag]);
190
- const extra = EXTRA_TAGS.filter((tag) => config[tag]).map((tag) => TAG_LABEL[tag]);
191
- const allowed = PRIVACY_TAGS.filter((tag) => config[tag].enabled && config[tag].value);
192
- const head = `## 当前注释配置(profile)\n\n`;
193
- const body = [
194
- `- 文件头:@file 固定为文件名;开启 ${on.join('、') || '无'};关闭 ${off.join('、') || '无'}。`,
195
- `- 函数契约:${contract.length > 0 ? contract.join('、') : '不要求'}。`,
196
- `- 更多标签:${extra.length > 0 ? `可选 ${extra.join('、')}` : '未启用'}。`,
197
- `- 检查器放行:${allowed.length > 0 ? allowed.join('、') : '无'}(隐私标签只有填了非空值才放行)。`,
198
- ];
199
- return head + body.join('\n');
208
+ for (const tag of [...CONTRACT_TAGS, ...EXTRA_TAGS]) {
209
+ if (config[tag])
210
+ on.push(TAG_LABEL[tag]);
211
+ else
212
+ off.push(TAG_LABEL[tag]);
213
+ }
214
+ const style = config.commentStyle === 'block' ? '/** ... */' : '///';
215
+ const line = `- 声明符号:${style}(函数体固定 //);开启 ${on.join('、')};关闭 ${off.join('、') || '无'}。`;
216
+ return `## 当前注释配置(profile)\n\n${line}`;
200
217
  }
package/lib/index.js CHANGED
@@ -32,6 +32,8 @@ const HeaderTag = Schema.object({
32
32
  value: Schema.string().default('').volatile(),
33
33
  });
34
34
  export const Config = Schema.object({
35
+ // 声明注释符号:`///` 或 Doxygen 块注释;缺省值由 comment-policy 兜底。
36
+ commentStyle: Schema.string().default('slash').volatile(),
35
37
  brief: Schema.object({
36
38
  enabled: Schema.boolean().default(true).volatile(),
37
39
  value: Schema.string().default('').volatile(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chengzhao-dev/dsh-cpp-workflow",
3
- "version": "0.6.6",
4
- "description": "写 C++ 源码、配 CMake、用 Clang 构建;头文件用 /// Doxygen 文档注释,函数体用 // 分段说明,文件头标签可在设置里改。 · Write C++ sources, configure CMake, and build with Clang; /// Doxygen on declarations, // section notes in bodies, and configurable file-header tags.",
3
+ "version": "0.6.8",
4
+ "description": "写 C++ 源码、配 CMake、用 Clang 构建;头文件用 /// Doxygen 文档注释,函数体用 // 分段说明,文件头标签可在设置里改。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "exports": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: building-cpp
3
- description: 在已经写好 CMake 的 C++11–C++23 工程上配置、构建和检查时使用:Ninja、compile_commands.json、clang-format、clang-tidy、clangd、environment.json 与 running-build.sh;按定构建、定工具配置、通过检查三步推进。要编写或重构 CMakeLists.txt / cmake 模块时先开 configuring-cmake。一次改动同时碰到源码和构建文件时,先打开 developing-cpp。不用于只改源码语义、文档、Python 或与 C++ 无关的文件。
3
+ description: 在已写好 CMake 的 C++11–C++23 工程上配置、构建和检查时使用:Ninja、compile_commands.json、clang-format、clang-tidy、clangd、environment.json 与 running-build.sh。不用于只改源码语义、文档、Python 或与 C++ 无关的文件。
4
4
  metadata:
5
5
  short-description: C++11–C++23 构建三步
6
6
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: configuring-cmake
3
- description: 在 C++11–C++23 工程里编写或重构 CMake 时使用:CMakeLists.txt、cmake 模块、target 与属性、依赖与输出目录、语言标准与编译选项、2 空格排版;新工程默认 C++23。改 .h/.cpp 源码先开 developing-cpp;只配置、构建或跑工具链时开 building-cpp。不用于 Quarto、Python 或非 C++ 文档。
3
+ description: 编写或重构 C++11–C++23 工程的 CMake 时使用:CMakeLists.txt、cmake 模块、target 属性、依赖与输出目录、语言标准;新工程默认 C++23。不用于 Quarto、Python 或非 C++ 文档。
4
4
  metadata:
5
5
  short-description: CMake 配置三步
6
6
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: developing-cpp
3
- description: 编写或修改 C++11–C++23 源码、头文件、命名、异常与源码注释时使用;新工程默认 C++23;按定行为、定边界、定声明、定实现四步推进。一次改动同时碰到源码和构建文件时,先打开本技能。不用于只改文档、Python、CMake、构建脚本或与 C++ 无关的文件。
3
+ description: 编写或修改 C++11–C++23 源码、头文件、命名、异常与源码注释时使用,新工程默认 C++23;一次改动同时碰到源码和构建文件时先打开本技能。不用于只改文档、Python、CMake、构建脚本或与 C++ 无关的文件。
4
4
  metadata:
5
5
  short-description: C++11–C++23 源码四步
6
6
  ---
@@ -22,7 +22,7 @@ metadata:
22
22
  | 增量修正 | 用户点了一个函数、一条诊断或一个文件 | 沿本技能步骤走,只改判为不合格的那一处,其余保持原样 |
23
23
  | 整段重构 | 新工程、新阶段、用户没点名范围,或改动跨过库与程序的边界 | 同样四步;第 1、2 步先定删留,不动函数体;第 3、4 步才改代码 |
24
24
 
25
- 两种走法共用这些约束:不发明阶段表和用户请求里没有的行为、类型和命令;选类型与写法时以项目声明的语言标准为上限;只改注释或格式时不改可观察行为;不覆盖已有源码;不安装工具链;代理侧不写 `wsl` 或 `bash -lc`。后一步发现前一步没做完,退回那一步,不在当前步补结构。拿不准先用增量修正;做到一半发现边界被打破,退回整段重构,从被打破的那一步重走。
25
+ 两种走法共用这些约束:不发明阶段表和用户请求里没有的行为、类型和命令;选类型与写法时以项目声明的语言标准为上限;只改注释或格式时不改可观察行为;不覆盖已有源码;不安装工具链;代理侧不写 `wsl` 或 `bash -lc`。计划、命名方案或注释措辞与条文、项目技能或用户明确要求冲突时,先列冲突位置、原文与出处,由用户选一条;不自行取舍,也不默认采用计划。后一步发现前一步没做完,退回那一步,不在当前步补结构。拿不准先用增量修正;做到一半发现边界被打破,退回整段重构,从被打破的那一步重走。
26
26
 
27
27
  ## 步骤
28
28
 
@@ -99,7 +99,7 @@ std::vector<std::vector<CellPosition>> BuildLines() {
99
99
 
100
100
  ```cpp
101
101
  // board.cpp 的 namespace tictactoe:行、列、对角线三组,坐标固定。
102
- constexpr std::array<std::array<CellPosition, 3>, 8> kLines{{
102
+ constexpr std::array<std::array<CellPosition, 3>, 8> kLines = {{
103
103
  {{{0, 0}, {0, 1}, {0, 2}}}, // 行 0
104
104
  {{{1, 0}, {1, 1}, {1, 2}}}, // 行 1
105
105
  {{{2, 0}, {2, 1}, {2, 2}}}, // 行 2
@@ -67,7 +67,7 @@ GameResult result_ = GameResult::kInProgress;
67
67
  有参数或有返回值的函数写两段:第一段写结果、失败方式或检查项;第二段按签名把标签写齐。
68
68
 
69
69
  1. 第一段:普通函数写正常返回什么、失败时返回什么或抛什么;布尔查询与 `operator==` 写「检查…是否…」。
70
- 2. 第二段:每个参数一条 `@param`;有返回值默认写 `@return`,成立条件已由函数名与返回类型说清时可省略;会抛才写 `@throws`。
70
+ 2. 第二段:每个参数一条 `@param`;有返回值默认写 `@return`,成立条件已由函数名与返回类型说清时可省略;会抛才写 `@throws`;`@pre`、`@post`、`@retval`、`@tparam`、`@note`、`@see` 默认不写,确有需要时再打开。
71
71
 
72
72
  ```cpp
73
73
  /// 检查坐标能否落子,不修改棋盘。
@@ -122,18 +122,18 @@ GameResult result_ = GameResult::kInProgress;
122
122
 
123
123
  ## 布局:放在哪一层、哪一行
124
124
 
125
- - `///` 只给声明与文件头:头文件里的类型、函数声明、枚举项、公开与私有数据成员,以及文件头的 `@file`/`@brief` 块。调用方和 Doxygen 从这里取契约。
125
+ - 文件头与声明注释用设置里选定的符号(默认 `///`,也可 `/** ... */`):头文件里的类型、函数声明、枚举项、公开与私有数据成员,以及文件头的 `@file`/`@brief` 块。调用方和 Doxygen 从这里取契约。
126
126
  - `///<` 只给枚举项和 `struct` 短字段的行尾;不要写在赋值、`break` 或普通语句后面。
127
127
  - `//` 给函数体内的阶段说明、局部变量、守卫理由和普通语句行尾;这些注释不接 Doxygen 文档。
128
128
  - `//` 也给工具指令与别种文件:`// NOLINTNEXTLINE(...)`、`#endif // MACRO`,以及 CMake、YAML 等非 C++ 文件。
129
- - 判断看位置:声明写契约,用 `///`;实现写理由,用 `//`。
129
+ - 判断看位置:声明写契约,用选定的文档符号;实现写理由,用 `//`。
130
130
  - 位置固定:文件头在最上方;类型注释紧贴类型声明;特殊成员(构造、析构、拷贝与移动)排在其他函数之前;函数注释紧贴函数声明;数据成员的主注释紧贴声明上一行,不写行尾补充。
131
131
  - 类数据成员(含私有)的主注释用 `///`;行尾 `///<` 只给枚举项和 `struct` 的短字段。
132
132
  - 注释和它说明的声明之间不留空行;同一个声明块内,空行只用来分组。
133
133
 
134
134
  ### 文件头
135
135
 
136
- 每个手写源文件开头写一段 doxygen 文件头:第一行 `/// @file <文件名>`,第二行 `/// @brief` 一句话说明这个文件定义或实现什么,用一行 `///` 隔开后,只写本文件自己的组成部分或边界。`@file` 的参数必须与文件名逐字相同,检查器会核对。`@brief` 默认写出;`@author`、`@date`、`@copyright`、`@version` 默认不写,只有插件配置里打开并填了非空值才写出,作者、年份和变更史不写进源码。删掉把读者指去另一个头文件、另一个目录或另一层的句子,也删掉「这一层不负责什么」。不写作者、版权年份、变更日志和外部资料路径。
136
+ 每个手写源文件开头写一段 doxygen 文件头:第一行写 `@file <文件名>`,第二行写 `@brief` 一句话说明这个文件定义或实现什么,用一行空注释隔开后,只写本文件自己的组成部分或边界。`@file` 的参数必须与文件名逐字相同,检查器会核对。`@brief` 默认写出;`@author`、`@date`、`@copyright`、`@version` 默认不写,只有插件配置里打开并填了非空值才写出,作者、年份和变更史不写进源码。删掉把读者指去另一个头文件、另一个目录或另一层的句子,也删掉「这一层不负责什么」。不写作者、版权年份、变更日志和外部资料路径。
137
137
 
138
138
  ```cpp
139
139
  /// @file board.h
@@ -142,12 +142,24 @@ GameResult result_ = GameResult::kInProgress;
142
142
  /// 棋盘保存九个棋盘格子的状态,提供查询、落子与清空棋盘。
143
143
  ```
144
144
 
145
+ 选择块注释时,同一段写成:
146
+
147
+ ```cpp
148
+ /**
149
+ * @file board.h
150
+ * @brief 定义井字棋棋盘、格子坐标与格子类型。
151
+ *
152
+ * 棋盘保存九个棋盘格子的状态,提供查询、落子与清空棋盘。
153
+ */
154
+ ```
155
+
145
156
  - C++ 源文件用以上格式;CMake 用 `#`,YAML/JSON 用 `#`。
146
157
  - 超过约 200 行的文件用空行分成几节,每节的说明写在一行 `///` 上,不画装饰线。
147
158
  - 文件头 3–7 行;`@file` 与 `@brief` 各占一行,后面只写本文件的范围与组成,不写实现步骤、作者和变更史。
148
159
 
149
160
  ## 排版:符号、宽度与标签
150
161
 
162
+ - 注释符号在插件设置里选择:`///` 是默认写法,`/** ... */` 是另一种 Doxygen 写法;文件头与声明注释统一用同一种,函数体内仍只用 `//`。块注释每行以 ` * ` 对齐,收尾单独一行 ` */`。
151
163
  - 行宽跟项目 `.clang-format` 的 `ColumnLimit` 走(Google 基线是 80 列,中文按两列计)。
152
164
  - 行尾注释与代码之间空两格;`///<` 只接在枚举项和 `struct` 短字段声明行尾。
153
165
  - `@param`、`@return`、`@throws` 标签保持英文;`@param` 后先写参数名,再写一句。
@@ -155,7 +167,7 @@ GameResult result_ = GameResult::kInProgress;
155
167
  - `.clang-format`、`.clang-tidy`、`.clangd`、CMake 的配置项旁边写「为什么这样设」,不写「这一项是什么」;配置注释贴在被解释的键上方。
156
168
  - 禁止:大段表格、装饰图案、表情;外部资料路径、跨仓库引用、团队流程;注释掉的代码;注释元叙述(「其余内容见某处」「两处不重复」)。
157
169
  - `///` 与 `@param` 不进入 `.qmd`、YAML、CSS。
158
- - 函数体内不写 `///`;`//` 用于实现说明与 `// NOLINTNEXTLINE(...)` 这类工具指令。
170
+ - 函数体内不写 `///` 或 `/** ... */`;`//` 用于实现说明与 `// NOLINTNEXTLINE(...)` 这类工具指令。
159
171
 
160
172
  ## 润色:删复述,统一句式
161
173
 
@@ -51,7 +51,7 @@
51
51
  - 纯数据聚合用 `struct`;有不变量的类型用 `class`,不变量在构造函数和公共成员函数里维持。
52
52
  - 单参数构造函数写 `explicit`;零参数构造函数不写。
53
53
  - 默认成员初始化写在声明处(NSDMI),不要在构造函数初始化列表里重复。
54
- - 默认用花括号初始化:`int count{};`、`std::vector<int> values{1, 2, 3};`。只有需要对比、`auto` 推导或赋值语义时用 `=`;不要用 `()` 初始化普通对象。
54
+ - 默认用花括号初始化:`int count{};`、`std::vector<int> values{1, 2, 3};`。只有需要对比、`auto` 推导或赋值语义时用 `=`;`name = {{` 只用于多行聚合常量表,单个对象和空初始化仍写 `count{}`;不要用 `()` 初始化普通对象。
55
55
  - 类声明顺序:public → protected → private;类型与别名 → 静态常量 → 工厂 → 构造/赋值/析构 → 其他函数 → 数据成员。
56
56
  - 用户声明的拷贝/移动/析构函数在头文件声明、在 `.cpp` 定义;只有零参数且默认实现足够时才写 `= default`。
57
57
  - 查询成员函数标尾部 `const`;返回结果对象的 API 加 `[[nodiscard]]`,不要给所有非 `void` 函数机械添加。
@@ -12,7 +12,7 @@
12
12
 
13
13
  ## 初始化与列表初始化
14
14
 
15
- 默认用花括号初始化,只在需要对比、`auto` 推导或赋值语义时用 `=`:
15
+ 默认用花括号初始化,只在需要对比、`auto` 推导或赋值语义时用 `=`;`name = {{` 只用于多行聚合常量表:
16
16
 
17
17
  ```cpp
18
18
  int count{}; // 值初始化,0
@@ -21,6 +21,7 @@ std::string name = "demo"; // 拷贝初始化,类型明确
21
21
  auto total = 0.0; // auto 只能用 =,不能用 {}
22
22
  ```
23
23
 
24
+ - `kLines` 这类多行聚合常量表写 `name = {{`;单个对象和空初始化仍写 `count{}`,不写 `=`;不把其他花括号初始化改成 `=`。
24
25
  - `T value{}` 做值初始化:内置类型得 0,类类型调用默认构造函数。
25
26
  - 花括号禁止窄化转换:`int x{3.5}` 编译失败,`int x = 3.5` 静默截断。需要截断时显式 `static_cast<int>(3.5)`。
26
27
  - `std::vector<int> values(3)` 是三个 0;`values{3}` 是一个元素 3。两种括号语义不同,不要凭习惯混用。
@@ -6,7 +6,7 @@
6
6
  // 不重写任何文件。
7
7
  //
8
8
  // 用法:
9
- // node checking-comments.mjs [--root DIR] [--width N] [--no-header] [--json] [--quiet]
9
+ // node checking-comments.mjs [--root DIR] [--width N] [--comment-style slash|block] [--no-header] [--json] [--quiet]
10
10
  //
11
11
  // 退出码:0 无问题,1 发现问题,2 用法或读取错误。
12
12
  import { readdirSync, readFileSync, statSync } from 'node:fs'
@@ -30,6 +30,8 @@ const PRIVACY_TAGS = ['@author', '@date', '@copyright', '@version']
30
30
  const FORBIDDEN_TAGS = [...STATIC_FORBIDDEN_TAGS, ...PRIVACY_TAGS]
31
31
  const DOXY_FILE = /^\/\/\/\s*@file\s+\S/
32
32
  const DOXY_BRIEF = /^\/\/\/\s*@brief\s+\S/
33
+ const BLOCK_FILE = /^@file\s+\S/
34
+ const BLOCK_BRIEF = /^@brief\s+\S/
33
35
  const MEMBER_LIST = /提供[^。]{0,40}、[^。]{0,40}(的功能|的能力|的接口)|功能[::]/
34
36
  const RATIONALE = /而不是|而非|代价.{0,4}忽略|只留一份|与其|比.{0,12}更|更(?:安全|快|清楚|直接|好写|难写)/
35
37
  const FORWARD_REF = /(?:留在|归|交给|由)\s*`?[A-Z][A-Za-z0-9_]*`?/
@@ -41,12 +43,12 @@ const DECL_KEYWORDS = /^(class|struct|union|enum|namespace|using|typedef|templat
41
43
 
42
44
  function usage(message) {
43
45
  if (message) process.stderr.write(`错误:${message}\n`)
44
- process.stderr.write('用法:node checking-comments.mjs [--root DIR] [--width N] [--no-header] [--no-brief] [--allow-tag TAG] [--json] [--quiet]\n')
46
+ process.stderr.write('用法:node checking-comments.mjs [--root DIR] [--width N] [--comment-style slash|block] [--no-header] [--no-brief] [--allow-tag TAG] [--json] [--quiet]\n')
45
47
  return EXIT.usage
46
48
  }
47
49
 
48
50
  function parseArgs(argv) {
49
- const options = { root: undefined, width: 80, widthExplicit: false, requireHeader: true, requireBrief: true, allowTags: new Set(), json: false, quiet: false }
51
+ const options = { root: undefined, width: 80, widthExplicit: false, commentStyle: 'slash', requireHeader: true, requireBrief: true, allowTags: new Set(), json: false, quiet: false }
50
52
  for (let index = 0; index < argv.length; index += 1) {
51
53
  const arg = argv[index]
52
54
  if (arg === '--root') {
@@ -56,6 +58,11 @@ function parseArgs(argv) {
56
58
  options.width = Number(argv[index + 1])
57
59
  options.widthExplicit = true
58
60
  index += 1
61
+ } else if (arg === '--comment-style') {
62
+ const value = argv[index + 1]
63
+ if (value !== 'slash' && value !== 'block') throw new Error('--comment-style 只接受 slash 或 block')
64
+ options.commentStyle = value
65
+ index += 1
59
66
  } else if (arg === '--no-header') {
60
67
  options.requireHeader = false
61
68
  } else if (arg === '--no-brief') {
@@ -198,6 +205,34 @@ function classMemberLines(lines) {
198
205
  return flags
199
206
  }
200
207
 
208
+ /**
209
+ * 取从 `index` 开始的 Doxygen 块注释;不是块注释或没有收尾时返回 undefined。
210
+ * `contents` 去掉开头标记、结尾标记与每行的 `*` 前缀,空行保留为空串。
211
+ */
212
+ export function blockDocAt(lines, index) {
213
+ const first = lines[index]?.trim() ?? ''
214
+ if (!first.startsWith('/**') || first.startsWith('/**<')) return undefined
215
+ const contents = []
216
+ if (first.includes('*/')) {
217
+ const inner = first.slice(first.indexOf('/**') + 3, first.lastIndexOf('*/')).trim()
218
+ if (inner) contents.push(inner.replace(/^\*\s?/, '').trim())
219
+ return { start: index, end: index, contents }
220
+ }
221
+ const opening = first.slice(first.indexOf('/**') + 3).trim().replace(/^\*\s?/, '')
222
+ if (opening) contents.push(opening)
223
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
224
+ const line = lines[cursor]
225
+ const trimmed = line.trim()
226
+ if (trimmed.includes('*/')) {
227
+ const before = trimmed.slice(0, trimmed.indexOf('*/')).replace(/^\*\s?/, '').trim()
228
+ if (before) contents.push(before)
229
+ return { start: index, end: cursor, contents }
230
+ }
231
+ contents.push(trimmed.replace(/^\*\s?/, '').trim())
232
+ }
233
+ return undefined
234
+ }
235
+
201
236
  /** 检查一段文本,返回问题列表;每个问题带 1 起始的行号。 */
202
237
  export function checkText(text, options = {}) {
203
238
  const width = options.width ?? 80
@@ -207,38 +242,52 @@ export function checkText(text, options = {}) {
207
242
  const add = (line, rule, message) => issues.push({ line, rule, message })
208
243
  const inClass = classMemberLines(lines)
209
244
 
245
+ const style = options.commentStyle === 'block' ? 'block' : 'slash'
210
246
  const firstContent = lines.findIndex((line) => line.trim() !== '')
211
247
  const firstTrimmed = firstContent === -1 ? '' : lines[firstContent].trim()
212
- const hasDoxyHeader = DOXY_FILE.test(firstTrimmed)
248
+ const headerBlock = style === 'block' && firstContent !== -1 ? blockDocAt(lines, firstContent) : undefined
249
+ const hasDoxyHeader = style === 'block'
250
+ ? Boolean(headerBlock && BLOCK_FILE.test(headerBlock.contents[0] ?? ''))
251
+ : DOXY_FILE.test(firstTrimmed)
213
252
  let doxyHeaderEnd = -1
214
- if (hasDoxyHeader) {
253
+ if (hasDoxyHeader && style === 'block') {
254
+ doxyHeaderEnd = headerBlock.end
255
+ } else if (hasDoxyHeader) {
215
256
  doxyHeaderEnd = firstContent
216
257
  while (doxyHeaderEnd + 1 < lines.length && lines[doxyHeaderEnd + 1].trim().startsWith('///')) doxyHeaderEnd += 1
217
258
  }
218
259
  if (requireHeader && firstContent !== -1 && !hasDoxyHeader) {
219
- add(firstContent + 1, 'header-missing', '文件开头缺少文件头:写 /// @file <文件名>,下一行写 /// @brief')
260
+ if (style === 'block') add(firstContent + 1, 'header-missing', '文件开头缺少文件头:写 /** ... */,第一行 @file <文件名>,下一行 @brief')
261
+ else add(firstContent + 1, 'header-missing', '文件开头缺少文件头:写 /// @file <文件名>,下一行写 /// @brief')
220
262
  }
221
263
  if (firstContent !== -1 && hasDoxyHeader) {
222
264
  const own = options.fileName ? options.fileName.split(/[\\/]/).pop() : ''
265
+ const declaredText = style === 'block' ? (headerBlock.contents[0] ?? '') : lines[firstContent].trim()
223
266
  if (own) {
224
- const declared = /@file\s+(\S+)/.exec(lines[firstContent].trim())
267
+ const declared = /@file\s+(\S+)/.exec(declaredText)
225
268
  if (declared && declared[1] !== own) {
226
269
  add(firstContent + 1, 'header-file-name', `文件头写的是 ${declared[1]},本文件是 ${own}:@file 必须等于文件名`)
227
270
  }
228
271
  }
229
- if (hasDoxyHeader && options.requireBrief !== false && !DOXY_BRIEF.test((lines[firstContent + 1] ?? '').trim())) {
230
- add(firstContent + 2, 'header-brief', '文件头第二行缺少 /// @brief 一句话说明')
272
+ if (options.requireBrief !== false) {
273
+ const hasBrief = style === 'block'
274
+ ? BLOCK_BRIEF.test(headerBlock.contents[1] ?? '')
275
+ : DOXY_BRIEF.test((lines[firstContent + 1] ?? '').trim())
276
+ if (!hasBrief) {
277
+ add(style === 'block' ? headerBlock.start + 3 : firstContent + 2, 'header-brief', '文件头第二行缺少 @brief 一句话说明')
278
+ }
231
279
  }
232
- for (let index = firstContent + 1; index < lines.length; index += 1) {
233
- const trimmed = lines[index].trim()
234
- if (!trimmed.startsWith('///')) break
235
- const names = trimmed.match(/\b[A-Za-z0-9_-]+\.(?:h|hpp|cpp|cc)\b/g) ?? []
236
- const fileMatches = trimmed.match(/@file\s+(\S+)/)
280
+ const headerLines = style === 'block'
281
+ ? headerBlock.contents.slice(1).map((content, offset) => ({ line: headerBlock.start + offset + 2, text: content }))
282
+ : lines.slice(firstContent + 1, doxyHeaderEnd + 1).map((content, offset) => ({ line: firstContent + offset + 2, text: content.trim() }))
283
+ for (const { line: headerLine, text } of headerLines) {
284
+ const names = text.match(/\b[A-Za-z0-9_-]+\.(?:h|hpp|cpp|cc)\b/g) ?? []
285
+ const fileMatches = text.match(/@file\s+(\S+)/)
237
286
  const namesToCheck = fileMatches ? names.filter((name) => name !== fileMatches[1]) : names
238
287
  for (const name of namesToCheck) {
239
- if (name !== own) add(index + 1, 'header-cross-file', `文件头提到了别的源文件 ${name}:只介绍本文件,删掉跨文件句子`)
288
+ if (name !== own) add(headerLine, 'header-cross-file', `文件头提到了别的源文件 ${name}:只介绍本文件,删掉跨文件句子`)
240
289
  }
241
- if (/\bapps\//.test(trimmed)) add(index + 1, 'header-cross-file', '文件头指向了另一个目录:只介绍本文件')
290
+ if (/\bapps\//.test(text)) add(headerLine, 'header-cross-file', '文件头指向了另一个目录:只介绍本文件')
242
291
  }
243
292
  }
244
293
 
@@ -254,6 +303,22 @@ export function checkText(text, options = {}) {
254
303
  }
255
304
  }
256
305
 
306
+ if (style === 'block') {
307
+ for (let index = 0; index < lines.length; index += 1) {
308
+ const doc = blockDocAt(lines, index)
309
+ if (!doc) continue
310
+ for (const content of doc.contents) {
311
+ const rendered = ` * ${content}`.trimEnd()
312
+ if (displayWidth(rendered) > width) add(doc.start + 1, 'comment-width', `注释行 ${displayWidth(rendered)} 列,超过 ${width} 列`)
313
+ for (const tag of FORBIDDEN_TAGS) {
314
+ if (options.allowTags?.has(tag.slice(1))) continue
315
+ if (content.includes(tag)) add(doc.start + 1, 'forbidden-tag', `禁止使用 ${tag},按 references/comments.md 的标签表改写`)
316
+ }
317
+ }
318
+ index = doc.end
319
+ }
320
+ }
321
+
257
322
  for (let index = 0; index < lines.length; index += 1) {
258
323
  if (doxyHeaderEnd !== -1 && index <= doxyHeaderEnd) {
259
324
  index = doxyHeaderEnd
@@ -310,6 +375,47 @@ export function checkText(text, options = {}) {
310
375
  }
311
376
  }
312
377
 
378
+ if (style === 'block') {
379
+ for (let index = 0; index < lines.length; index += 1) {
380
+ if (doxyHeaderEnd !== -1 && index <= doxyHeaderEnd) {
381
+ index = doxyHeaderEnd
382
+ continue
383
+ }
384
+ const doc = blockDocAt(lines, index)
385
+ if (!doc) continue
386
+ index = doc.end
387
+ let next = doc.end + 1
388
+ if (next < lines.length && lines[next].trim() === '') {
389
+ add(doc.end + 1, 'doc-gap', '注释与声明之间有空行:注释会挂到下一个声明,请删掉空行')
390
+ while (next < lines.length && lines[next].trim() === '') next += 1
391
+ }
392
+ if (next >= lines.length) continue
393
+ const declaration = lines[next].trim()
394
+ const block = doc.contents.join('\n')
395
+ const isType = /^(template\s*<[^>]*>\s*)?(class|struct|union|enum)\b/.test(declaration)
396
+ if (RATIONALE.test(block)) add(doc.start + 1, 'rationale', '注释在写设计取舍:方案对比与理由移到正文或实现注释,接口注释只留契约')
397
+ if (isType && MEMBER_LIST.test(block)) add(doc.start + 1, 'member-list', '类型注释在罗列成员功能,应改写为概念与使用约定')
398
+ if (isType && FORWARD_REF.test(block)) add(doc.start + 1, 'forward-ref', '类型注释在交代别的类型负责什么:只写本类型的契约,删掉这句,不要挪到文件头')
399
+ if (isType && TYPE_SYSTEM.test(block)) add(doc.start + 1, 'type-system', '类型注释在复述类型系统已表达的性质:可拷贝与 const 查询由声明给出,删掉这句')
400
+ if (isType && NEGATED_BOUNDARY.test(block)) add(doc.start + 1, 'negated-boundary', '类型注释用「只保存/不拥有/不含」写否定式边界:改成名词短语,边界交给成员类型表达')
401
+ if (isType && DOMAIN_SIZE.test(block)) add(doc.start + 1, 'domain-size', '类型注释在写领域尺寸:格子数由 cells_ 之类成员声明表达,删掉这句')
402
+ const firstDocLine = doc.contents[0] ?? ''
403
+ if (!isType && PREDICATE_DECL.test(declaration) && !firstDocLine.startsWith('检查')) {
404
+ add(doc.start + 1, 'predicate-first', '布尔查询或 operator== 的第一句应写「检查…是否…」')
405
+ }
406
+ const params = functionParams(declaration)
407
+ if (!params) continue
408
+ const documented = [...block.matchAll(/@param(?:\[[^\]]*\])?\s+(\w+)/g)].map((match) => match[1])
409
+ if (documented.length === 0) continue
410
+ for (const name of params) {
411
+ if (!documented.includes(name)) add(doc.start + 1, 'param-missing', `参数 ${name} 没有 @param 说明`)
412
+ }
413
+ for (const name of documented) {
414
+ if (!params.includes(name)) add(doc.start + 1, 'param-extra', `@param ${name} 在声明里不存在`)
415
+ }
416
+ }
417
+ }
418
+
313
419
  for (let index = 0; index + 1 < lines.length; index += 1) {
314
420
  const trimmed = lines[index].trim()
315
421
  if (!trimmed.startsWith('//') || trimmed.startsWith('///') || trimmed.startsWith('//!')) continue