mirror of
https://github.com/motajs/template.git
synced 2026-11-01 06:04:30 +08:00
fix(event): 采纳七个内建并修正 touched-jsdoc 门禁
- event.test.ts 与 03-EVENT-CONTRACT.md 同步为七个内建(移除 touchFront/EventTouchFront) - check-touched-jsdoc 按 dev.md 豁免继承/implements 成员,补充防误删说明 - 刷新 Phase 01-03 验证指纹,三个阶段恢复 passed
This commit is contained in:
1 parent
a55c2e32a2
commit
c80516deb7
6 files changed
+106
-53
No files matched your search
@@ -1,3 +1,18 @@
|
||||
// 门禁:检查「显式传入的文件」中的非构造器声明是否带多行 JSDoc。
|
||||
//
|
||||
// 背景:本脚本由 Phase 03 Plan 03-19 引入,只用于校验本次修正触及的 replay/event
|
||||
// 文件,不是仓库级 lint,也未接入 CI / pre-commit,仅由 verify 命令按文件名显式调用。
|
||||
//
|
||||
// 检查范围:
|
||||
// - 只盘点顶层 function 声明与类的方法 / getter / setter。
|
||||
// - 不盘点 interface 成员、类型别名、枚举、类属性、对象字面量及箭头函数成员。
|
||||
// - 判定只看声明前是否存在多行 /** ... */(开符号单独占行、结束符号单独占行)。
|
||||
//
|
||||
// 重要:本脚本不读取注释内容,也不判断注释是否正确、过时或重复。MISSING 只表示
|
||||
// 「缺少多行 JSDoc」这一格式问题,与已有注释的内容无关。修复 MISSING 时不得删除、
|
||||
// 改写或降级任何已有注释:应补一段多行 JSDoc;若确属继承 / implements 而来的 API,
|
||||
// 则依赖 hasBaseMember 的豁免规则(对应 dev.md「继承或 implements 而来的 API 不应
|
||||
// 重复添加 jsDoc」)。
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { relative, resolve } from 'node:path';
|
||||
import ts from 'typescript';
|
||||
@@ -27,6 +42,22 @@ function hasMultilineJsDoc(node: ts.Node, source: ts.SourceFile): boolean {
|
||||
return /\r?\n\s*\*\/$/.test(comment);
|
||||
}
|
||||
|
||||
// dev.md:继承或 implements 而来的 API 在实现处不应重复添加 JSDoc,
|
||||
// 其说明由源头负责;外部库(如 @motajs/anon-tokyo)的源头不在本仓库检查范围内
|
||||
function hasBaseMember(
|
||||
classDecl: ts.ClassDeclaration,
|
||||
memberName: string,
|
||||
checker: ts.TypeChecker
|
||||
): boolean {
|
||||
for (const clause of classDecl.heritageClauses ?? []) {
|
||||
for (const typeNode of clause.types) {
|
||||
const baseType = checker.getTypeAtLocation(typeNode);
|
||||
if (baseType.getProperty(memberName)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function createEntry(
|
||||
file: string,
|
||||
symbol: string,
|
||||
@@ -50,7 +81,8 @@ function createEntry(
|
||||
|
||||
function collectEntries(
|
||||
file: string,
|
||||
source: ts.SourceFile
|
||||
source: ts.SourceFile,
|
||||
checker: ts.TypeChecker | null
|
||||
): IInventoryEntry[] {
|
||||
const entries: IInventoryEntry[] = [];
|
||||
for (const statement of source.statements) {
|
||||
@@ -88,14 +120,18 @@ function collectEntries(
|
||||
ts.isGetAccessorDeclaration(member) ||
|
||||
ts.isSetAccessorDeclaration(member)
|
||||
) {
|
||||
const name = member.name.getText(source);
|
||||
const inherited = checker
|
||||
? hasBaseMember(statement, name, checker)
|
||||
: false;
|
||||
entries.push(
|
||||
createEntry(
|
||||
file,
|
||||
`${owner}.${member.name.getText(source)}`,
|
||||
`${owner}.${name}`,
|
||||
'method',
|
||||
member,
|
||||
source,
|
||||
false
|
||||
inherited
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -104,16 +140,45 @@ function collectEntries(
|
||||
return entries;
|
||||
}
|
||||
|
||||
function readEntries(file: string): IInventoryEntry[] {
|
||||
const text = readFileSync(file, 'utf8');
|
||||
const source = ts.createSourceFile(
|
||||
file,
|
||||
text,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TS
|
||||
);
|
||||
return collectEntries(toDisplayPath(file), source);
|
||||
function createCheckedProgram(files: readonly string[]): ts.Program | null {
|
||||
try {
|
||||
const configPath = ts.findConfigFile(
|
||||
process.cwd(),
|
||||
ts.sys.fileExists,
|
||||
'tsconfig.json'
|
||||
);
|
||||
if (!configPath) return null;
|
||||
const config = ts.readConfigFile(configPath, ts.sys.readFile);
|
||||
if (config.error) return null;
|
||||
const parsed = ts.parseJsonConfigFileContent(
|
||||
config.config,
|
||||
ts.sys,
|
||||
process.cwd()
|
||||
);
|
||||
return ts.createProgram(
|
||||
files.map(file => resolve(file)),
|
||||
parsed.options
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readEntries(
|
||||
file: string,
|
||||
program: ts.Program | null,
|
||||
checker: ts.TypeChecker | null
|
||||
): IInventoryEntry[] {
|
||||
const source =
|
||||
program?.getSourceFile(resolve(file)) ??
|
||||
ts.createSourceFile(
|
||||
file,
|
||||
readFileSync(file, 'utf8'),
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TS
|
||||
);
|
||||
return collectEntries(toDisplayPath(file), source, checker);
|
||||
}
|
||||
|
||||
function report(entries: readonly IInventoryEntry[]): number {
|
||||
@@ -132,7 +197,7 @@ function report(entries: readonly IInventoryEntry[]): number {
|
||||
const constructors = entries.filter(entry => entry.exempt);
|
||||
if (constructors.length > 0) {
|
||||
console.log(
|
||||
`Constructors explicitly exempt from JSDoc: ${constructors
|
||||
`Declarations exempt from JSDoc (constructor or inherited): ${constructors
|
||||
.map(entry => entry.symbol)
|
||||
.join(', ')}`
|
||||
);
|
||||
@@ -150,11 +215,14 @@ function report(entries: readonly IInventoryEntry[]): number {
|
||||
` ${violation.file}:${violation.line} ${violation.symbol} [${violation.kind}]`
|
||||
);
|
||||
}
|
||||
console.error(
|
||||
' 注意:MISSING 仅表示缺少多行 JSDoc,与注释内容无关;修复时不得删除或改写已有注释'
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
console.log(
|
||||
'check-touched-jsdoc passed: every non-constructor declaration has multiline JSDoc'
|
||||
'check-touched-jsdoc passed: every non-exempt declaration has multiline JSDoc'
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
@@ -167,7 +235,14 @@ function main(): void {
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const entries = files.flatMap(readEntries);
|
||||
const program = createCheckedProgram(files);
|
||||
const checker = program?.getTypeChecker() ?? null;
|
||||
if (!checker) {
|
||||
console.error(
|
||||
'check-touched-jsdoc warning: TypeScript program unavailable — inherited members cannot be detected'
|
||||
);
|
||||
}
|
||||
const entries = files.flatMap(file => readEntries(file, program, checker));
|
||||
if (report(entries) !== 0) process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user