@chengzhao-dev/dsh-cpp-workflow 0.6.7 → 0.6.9
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.
- package/README.en.md +3 -3
- package/README.md +4 -4
- package/lib/client.js +353 -287
- package/lib/comment-policy.js +81 -62
- package/lib/index.js +20 -14
- package/package.json +2 -2
- package/skills/building-cpp/SKILL.md +1 -1
- package/skills/configuring-cmake/SKILL.md +1 -1
- package/skills/developing-cpp/SKILL.md +2 -2
- package/skills/developing-cpp/examples/structure.md +1 -1
- package/skills/developing-cpp/references/comments.md +27 -6
- package/skills/developing-cpp/references/google-cpp-style.md +2 -2
- package/skills/developing-cpp/references/language.md +2 -1
- package/skills/developing-cpp/references/types.md +1 -1
- package/skills/developing-cpp/scripts/checking-comments.mjs +139 -17
- package/skills/developing-cpp/steps/03-declaration.md +1 -1
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
// 只读注释审计:按 references/comments.md 的硬规则检查 C/C++ 源文件。
|
|
3
3
|
//
|
|
4
4
|
// 检查项:Doxygen 文件头、注释行宽度、标签、注释与声明的贴合、
|
|
5
|
-
// 类型注释存在性、成员功能清单、设计论证(方案对比)、否定式边界、领域尺寸、谓词首句、@param
|
|
5
|
+
// 类型注释存在性、成员功能清单、设计论证(方案对比)、否定式边界、领域尺寸、谓词首句、@param 完整性、枚举成员注释一致性、枚举行尾句式、数据成员主注释位置。
|
|
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
|
|
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', '文件开头缺少文件头:写
|
|
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(
|
|
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 (
|
|
230
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
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(
|
|
288
|
+
if (name !== own) add(headerLine, 'header-cross-file', `文件头提到了别的源文件 ${name}:只介绍本文件,删掉跨文件句子`)
|
|
240
289
|
}
|
|
241
|
-
if (/\bapps\//.test(
|
|
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
|
|
@@ -376,6 +482,22 @@ export function checkText(text, options = {}) {
|
|
|
376
482
|
index = body
|
|
377
483
|
}
|
|
378
484
|
|
|
485
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
486
|
+
const marker = lines[index].indexOf('///<')
|
|
487
|
+
if (marker === -1) continue
|
|
488
|
+
const content = lines[index].slice(marker + 4).trim().replace(/。$/, '').trim()
|
|
489
|
+
const colon = content.indexOf(':')
|
|
490
|
+
if (colon !== -1) {
|
|
491
|
+
if (content.slice(colon + 1).trim() === '') {
|
|
492
|
+
add(index + 1, 'enum-trailing-empty', '行尾注释冒号后为空:写「类别:说明」,冒号两侧都不能为空')
|
|
493
|
+
}
|
|
494
|
+
continue
|
|
495
|
+
}
|
|
496
|
+
if (/[,,]/.test(content)) {
|
|
497
|
+
add(index + 1, 'enum-trailing-comma', '行尾注释用逗号连接类别与说明:改成「类别:说明」,类别在前')
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
379
501
|
for (let index = 0; index < lines.length; index += 1) {
|
|
380
502
|
const trimmed = lines[index].trim()
|
|
381
503
|
if (trimmed.startsWith('//') || trimmed.startsWith('///') || !/\bnoexcept\b/.test(trimmed)) continue
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
**异常声明**:构造函数、`Get` 类查询、解析函数最容易抛;在声明注释里写「当……时抛出 `std::out_of_range`」这样的具体类型,不要只写「可能抛异常」,也不要给每个参数错误都新建类型。类型选择见 `references/exceptions.md`。
|
|
27
27
|
|
|
28
|
-
**注释**:声明注释用 `///` 写中文散文,只写调用方需要知道的;实现细节留给 `.cpp`。`@param` 每个参数都写,`@return` 每个非 `void` 函数都写,`@throws` 会抛才写;构造、析构与 `void` 无参数函数不写空标签。标识符和标签保持英文。文件头默认写 `@file` 与 `@brief` 两行,`@file`
|
|
28
|
+
**注释**:声明注释用 `///` 写中文散文,只写调用方需要知道的;实现细节留给 `.cpp`。`@param` 每个参数都写,`@return` 每个非 `void` 函数都写,`@throws` 会抛才写;构造、析构与 `void` 无参数函数不写空标签。标识符和标签保持英文。文件头默认写 `@file` 与 `@brief` 两行,`@file` 必须与文件名相同;作者、日期、版权、版本开关出厂打开,填了非空值才写出。标签开关在插件设置页,技能正文末尾附带当前配置的快照。写完运行 `node "<skill-dir>/scripts/checking-comments.mjs" --root <项目根>`;关闭 `@brief` 时加 `--no-brief`,配置里打开了隐私标签并填了值时加 `--allow-tag <标签>`。文件头、标签、参数完整性、成员清单与设计论证由脚本兜底。文件头与 TODO 的格式见 `references/comments.md`。
|
|
29
29
|
|
|
30
30
|
## 正反例指针
|
|
31
31
|
|