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:
unanmed 2026-09-12 13:24:06 +08:00
parent b1b9603ff6
commit cabee3cc01
2 changed files with 230 additions and 2 deletions

View File

@ -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>

View 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);
}