mirror of
https://github.com/motajs/template.git
synced 2026-09-18 13:10:21 +08:00
style(03-19): normalize touched replay JSDoc to multiline
- Convert replay command, helper, and registry JSDoc into multiline form - Add declaration-aware script/check-touched-jsdoc.ts scanner that inventories constructors as explicit exemptions and fails any other touched top-level function or class method without multiline JSDoc
This commit is contained in:
parent
b1b9603ff6
commit
cabee3cc01
@ -13,22 +13,37 @@ import {
|
||||
REPLAY_COMMAND_ORDER
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* 判断未知值是否为有限数值
|
||||
*/
|
||||
function isNumber(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断未知值是否为可用的道具编号或道具 id
|
||||
*/
|
||||
function isItem(value: unknown): value is number | string {
|
||||
return isNumber(value) || typeof value === 'string';
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断未知值是否为布尔值
|
||||
*/
|
||||
function isBoolean(value: unknown): value is boolean {
|
||||
return typeof value === 'boolean';
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断未知值是否为可用的装备槽位编号或槽位 id
|
||||
*/
|
||||
function isSlot(value: unknown): value is number | string {
|
||||
return isNumber(value) || typeof value === 'string';
|
||||
}
|
||||
|
||||
/**
|
||||
* 将槽位编号或槽位 id 解析为装备槽位索引
|
||||
*/
|
||||
function resolveSlot(
|
||||
state: IReplayCommandState,
|
||||
slot: number | string
|
||||
@ -46,6 +61,9 @@ class ReplayDirectionCommand implements IReplayCommand {
|
||||
private readonly direction: FaceDirection
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 按构造方向启动一次勇士移动并等待移动结束
|
||||
*/
|
||||
private async moveHero(): Promise<boolean> {
|
||||
try {
|
||||
const mover = this.state.hero.location.mover;
|
||||
@ -60,6 +78,9 @@ class ReplayDirectionCommand implements IReplayCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验录像步参数并执行一次方向移动
|
||||
*/
|
||||
execute(step: IReplayStepHandler): Promise<boolean> {
|
||||
if (step.params.length !== 0) return Promise.resolve(false);
|
||||
return this.moveHero();
|
||||
@ -69,6 +90,9 @@ class ReplayDirectionCommand implements IReplayCommand {
|
||||
class ReplayAutoPathfindCommand implements IReplayCommand {
|
||||
constructor(private readonly state: IReplayCommandState) {}
|
||||
|
||||
/**
|
||||
* 自动寻路到目标坐标并等待寻路结束
|
||||
*/
|
||||
private async moveToPoint(x: number, y: number): Promise<boolean> {
|
||||
try {
|
||||
const result = this.state.pathfinding.moveTo({ x, y });
|
||||
@ -80,6 +104,9 @@ class ReplayAutoPathfindCommand implements IReplayCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验录像步参数并执行一次自动寻路
|
||||
*/
|
||||
execute(step: IReplayStepHandler): Promise<boolean> {
|
||||
if (step.params.length !== 2) return Promise.resolve(false);
|
||||
const x = step.params[0];
|
||||
@ -92,10 +119,16 @@ class ReplayAutoPathfindCommand implements IReplayCommand {
|
||||
class ReplayUseItemCommand implements IReplayCommand {
|
||||
constructor(private readonly state: IReplayCommandState) {}
|
||||
|
||||
/**
|
||||
* 使用指定道具并返回现有状态接口的结果
|
||||
*/
|
||||
private useItem(item: number | string): boolean {
|
||||
return this.state.hero.items.useItem(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验录像步参数并执行一次道具使用
|
||||
*/
|
||||
execute(step: IReplayStepHandler): Promise<boolean> {
|
||||
if (step.params.length !== 1) return Promise.resolve(false);
|
||||
const item = step.params[0];
|
||||
@ -107,6 +140,9 @@ class ReplayUseItemCommand implements IReplayCommand {
|
||||
class ReplayEquipCommand implements IReplayCommand {
|
||||
constructor(private readonly state: IReplayCommandState) {}
|
||||
|
||||
/**
|
||||
* 将指定装备穿到目标槽位并返回是否穿装成功
|
||||
*/
|
||||
private equip(
|
||||
uid: number,
|
||||
slot: number | string,
|
||||
@ -123,6 +159,9 @@ class ReplayEquipCommand implements IReplayCommand {
|
||||
return equipment.getEquipped(slotIndex) === uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验录像步参数并执行一次装备穿装
|
||||
*/
|
||||
execute(step: IReplayStepHandler): Promise<boolean> {
|
||||
if (step.params.length < 2 || step.params.length > 3) {
|
||||
return Promise.resolve(false);
|
||||
@ -145,6 +184,9 @@ class ReplayEquipCommand implements IReplayCommand {
|
||||
class ReplayUnequipCommand implements IReplayCommand {
|
||||
constructor(private readonly state: IReplayCommandState) {}
|
||||
|
||||
/**
|
||||
* 卸下指定槽位的装备并返回是否卸下成功
|
||||
*/
|
||||
private unequip(slot: number): boolean {
|
||||
const equipment = this.state.hero.equip;
|
||||
if (equipment.getEquipped(slot) === undefined) return false;
|
||||
@ -152,6 +194,9 @@ class ReplayUnequipCommand implements IReplayCommand {
|
||||
return equipment.getEquipped(slot) === undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验录像步参数并执行一次装备卸下
|
||||
*/
|
||||
execute(step: IReplayStepHandler): Promise<boolean> {
|
||||
if (step.params.length !== 1) return Promise.resolve(false);
|
||||
const slot = step.params[0];
|
||||
@ -162,7 +207,9 @@ class ReplayUnequipCommand implements IReplayCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/** 创建按稳定 enum 顺序排列的默认 replay command items */
|
||||
/**
|
||||
* 创建按稳定 enum 顺序排列的默认 replay command items
|
||||
*/
|
||||
export function createReplayCommandItems(
|
||||
state: IReplayCommandState
|
||||
): ReadonlyArray<IReplayCommandItem> {
|
||||
@ -202,7 +249,9 @@ export function createReplayCommandItems(
|
||||
];
|
||||
}
|
||||
|
||||
/** 按 top-level stable code 注册 command,并在注册前拒绝重复项 */
|
||||
/**
|
||||
* 按 top-level stable code 注册 command,并在注册前拒绝重复项
|
||||
*/
|
||||
export function registerReplayCommandItems(
|
||||
replay: IReplaySystem | IReplayCommandRegistry,
|
||||
items: ReadonlyArray<IReplayCommandItem>
|
||||
|
||||
179
script/check-touched-jsdoc.ts
Normal file
179
script/check-touched-jsdoc.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { relative, resolve } from 'node:path';
|
||||
import ts from 'typescript';
|
||||
|
||||
type DeclarationKind = 'constructor' | 'function' | 'method';
|
||||
|
||||
interface IInventoryEntry {
|
||||
readonly file: string;
|
||||
readonly symbol: string;
|
||||
readonly kind: DeclarationKind;
|
||||
readonly line: number;
|
||||
readonly exempt: boolean;
|
||||
readonly hasMultilineJsDoc: boolean;
|
||||
}
|
||||
|
||||
function toDisplayPath(file: string): string {
|
||||
return relative(process.cwd(), resolve(file)).replaceAll('\\', '/');
|
||||
}
|
||||
|
||||
function hasMultilineJsDoc(node: ts.Node, source: ts.SourceFile): boolean {
|
||||
const ranges = ts.getLeadingCommentRanges(source.text, node.getFullStart());
|
||||
if (!ranges || ranges.length === 0) return false;
|
||||
const range = ranges[ranges.length - 1];
|
||||
const comment = source.text.slice(range.pos, range.end);
|
||||
if (!comment.startsWith('/**')) return false;
|
||||
if (!/^\/\*\*\r?\n/.test(comment)) return false;
|
||||
return /\r?\n\s*\*\/$/.test(comment);
|
||||
}
|
||||
|
||||
function createEntry(
|
||||
file: string,
|
||||
symbol: string,
|
||||
kind: DeclarationKind,
|
||||
node: ts.Node,
|
||||
source: ts.SourceFile,
|
||||
exempt: boolean
|
||||
): IInventoryEntry {
|
||||
const position = source.getLineAndCharacterOfPosition(
|
||||
node.getStart(source)
|
||||
);
|
||||
return {
|
||||
file,
|
||||
symbol,
|
||||
kind,
|
||||
line: position.line + 1,
|
||||
exempt,
|
||||
hasMultilineJsDoc: hasMultilineJsDoc(node, source)
|
||||
};
|
||||
}
|
||||
|
||||
function collectEntries(
|
||||
file: string,
|
||||
source: ts.SourceFile
|
||||
): IInventoryEntry[] {
|
||||
const entries: IInventoryEntry[] = [];
|
||||
for (const statement of source.statements) {
|
||||
if (ts.isFunctionDeclaration(statement) && statement.name) {
|
||||
entries.push(
|
||||
createEntry(
|
||||
file,
|
||||
statement.name.text,
|
||||
'function',
|
||||
statement,
|
||||
source,
|
||||
false
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!ts.isClassDeclaration(statement) || !statement.name) continue;
|
||||
const owner = statement.name.text;
|
||||
for (const member of statement.members) {
|
||||
if (ts.isConstructorDeclaration(member)) {
|
||||
entries.push(
|
||||
createEntry(
|
||||
file,
|
||||
`${owner}.constructor`,
|
||||
'constructor',
|
||||
member,
|
||||
source,
|
||||
true
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
ts.isMethodDeclaration(member) ||
|
||||
ts.isGetAccessorDeclaration(member) ||
|
||||
ts.isSetAccessorDeclaration(member)
|
||||
) {
|
||||
entries.push(
|
||||
createEntry(
|
||||
file,
|
||||
`${owner}.${member.name.getText(source)}`,
|
||||
'method',
|
||||
member,
|
||||
source,
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 report(entries: readonly IInventoryEntry[]): number {
|
||||
console.log(`Touched JSDoc inventory: ${entries.length} declarations`);
|
||||
for (const entry of entries) {
|
||||
const status = entry.exempt
|
||||
? 'EXEMPT'
|
||||
: entry.hasMultilineJsDoc
|
||||
? 'MULTILINE'
|
||||
: 'MISSING';
|
||||
console.log(
|
||||
`INVENTORY ${entry.file}:${entry.line} ${entry.symbol} [${entry.kind}] ${status}`
|
||||
);
|
||||
}
|
||||
|
||||
const constructors = entries.filter(entry => entry.exempt);
|
||||
if (constructors.length > 0) {
|
||||
console.log(
|
||||
`Constructors explicitly exempt from JSDoc: ${constructors
|
||||
.map(entry => entry.symbol)
|
||||
.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
const violations = entries.filter(
|
||||
entry => !entry.exempt && !entry.hasMultilineJsDoc
|
||||
);
|
||||
if (violations.length > 0) {
|
||||
console.error(
|
||||
'check-touched-jsdoc failed: multiline JSDoc missing for'
|
||||
);
|
||||
for (const violation of violations) {
|
||||
console.error(
|
||||
` ${violation.file}:${violation.line} ${violation.symbol} [${violation.kind}]`
|
||||
);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
console.log(
|
||||
'check-touched-jsdoc passed: every non-constructor declaration has multiline JSDoc'
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const files = process.argv.slice(2);
|
||||
if (files.length === 0) {
|
||||
console.error(
|
||||
'Usage: pnpm exec tsx script/check-touched-jsdoc.ts <file...>'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const entries = files.flatMap(readEntries);
|
||||
if (report(entries) !== 0) process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exit(2);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user