diff --git a/scripts/husky-fast-check.mjs b/scripts/husky-fast-check.mjs index 2f578fa..a042575 100644 --- a/scripts/husky-fast-check.mjs +++ b/scripts/husky-fast-check.mjs @@ -90,7 +90,7 @@ function getStagedFiles() { .filter(Boolean); } -run(getPnpmCommand(), ['run', 'check:jsdoc']); +run(getPnpmCommand(), ['run', 'check:jsdoc', '--', '--staged']); const files = getStagedFiles().filter((file) => { if (!existsSync(file)) return false; diff --git a/scripts/jsdoc/check-jsdoc-policy.mjs b/scripts/jsdoc/check-jsdoc-policy.mjs index 5f44471..7101491 100644 --- a/scripts/jsdoc/check-jsdoc-policy.mjs +++ b/scripts/jsdoc/check-jsdoc-policy.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { execFileSync } from 'node:child_process'; -import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -17,35 +17,168 @@ const SOURCE_EXTENSIONS = new Set([ ]); const SOURCE_PATHSPECS = ['*.cjs', '*.js', '*.mjs', '*.ts', '*.tsx', '*.vue']; const CHINESE_CHARACTER_PATTERN = /\p{Script=Han}/u; +const LINE_TERMINATOR_PATTERN = /\r\n|\r|\n/u; const VUE_SCRIPT_PATTERN = /]*)>([\s\S]*?)<\/script\s*>/giu; +const TYPEORM_ENTITY_LISTENER_DECORATORS = new Set([ + 'AfterInsert', + 'AfterLoad', + 'AfterRecover', + 'AfterRemove', + 'AfterSoftRemove', + 'AfterUpdate', + 'BeforeInsert', + 'BeforeRecover', + 'BeforeRemove', + 'BeforeSoftRemove', + 'BeforeUpdate', +]); +const CODE_ONLY_TAGS = new Set(['example']); +const STRUCTURAL_TAGS = new Set([ + 'alias', + 'author', + 'borrows', + 'callback', + 'constant', + 'constructs', + 'copyright', + 'default', + 'defaultvalue', + 'enum', + 'exports', + 'extends', + 'external', + 'fires', + 'implements', + 'kind', + 'license', + 'link', + 'listens', + 'member', + 'memberof', + 'mixes', + 'module', + 'name', + 'namespace', + 'requires', + 'satisfies', + 'since', + 'this', + 'type', + 'typedef', + 'version', +]); +const BARE_TYPE_TAGS = new Set([ + 'exception', + 'return', + 'returns', + 'throws', + 'yield', + 'yields', +]); +const PARAMETER_TAGS = new Set([ + 'arg', + 'argument', + 'param', + 'prop', + 'property', + 'template', + 'typeparam', +]); +const BUILTIN_TYPE_NAMES = new Set([ + 'any', + 'bigint', + 'boolean', + 'never', + 'null', + 'number', + 'object', + 'string', + 'symbol', + 'undefined', + 'unknown', + 'void', +]); +const ENGLISH_FUNCTION_WORDS = new Set([ + 'a', + 'after', + 'an', + 'and', + 'are', + 'as', + 'at', + 'before', + 'by', + 'for', + 'from', + 'if', + 'in', + 'into', + 'is', + 'it', + 'its', + 'of', + 'on', + 'or', + 'otherwise', + 'that', + 'the', + 'these', + 'this', + 'those', + 'to', + 'was', + 'were', + 'when', + 'whether', + 'with', +]); +const LEADING_DESCRIPTION_VERBS = new Set([ + 'build', + 'check', + 'cleanup', + 'convert', + 'create', + 'delete', + 'determine', + 'ensure', + 'fetch', + 'format', + 'get', + 'handle', + 'initialize', + 'load', + 'parse', + 'read', + 'remove', + 'render', + 'resolve', + 'return', + 'save', + 'send', + 'set', + 'transform', + 'update', + 'use', + 'validate', + 'write', +]); const LIFECYCLE_ENTRY_NAMES = new Set([ 'activated', 'afterInit', - 'afterInsert', - 'afterLoad', 'afterQuery', - 'afterRecover', - 'afterRemove', - 'afterSoftRemove', 'afterTransactionCommit', 'afterTransactionRollback', 'afterTransactionStart', - 'afterUpdate', 'beforeApplicationShutdown', 'beforeCreate', 'beforeDestroy', - 'beforeInsert', 'beforeMount', 'beforeModuleDestroy', 'beforeQuery', - 'beforeRecover', - 'beforeRemove', - 'beforeSoftRemove', 'beforeTransactionCommit', 'beforeTransactionRollback', 'beforeTransactionStart', 'beforeUnmount', - 'beforeUpdate', 'componentDidCatch', 'componentDidMount', 'componentDidUpdate', @@ -148,10 +281,408 @@ function getNodeName(node, sourceFile) { return node.name.text; } + if (ts.isComputedPropertyName(node.name)) { + const expression = node.name.expression; + if (ts.isStringLiteralLike(expression) || ts.isNumericLiteral(expression)) { + return expression.text; + } + } + return node.name.getText(sourceFile); } -function classifyOwner(node, sourceFile) { +function getFunctionExpressionRole(node, sourceFile) { + let expression = node; + while ( + (ts.isParenthesizedExpression(expression.parent) || + ts.isAsExpression(expression.parent) || + ts.isTypeAssertionExpression(expression.parent) || + ts.isNonNullExpression(expression.parent) || + ts.isSatisfiesExpression(expression.parent)) && + expression.parent.expression === expression + ) { + expression = expression.parent; + } + + const parent = expression.parent; + if ( + (ts.isPropertyAssignment(parent) || ts.isPropertyDeclaration(parent)) && + parent.initializer === expression + ) { + return getNodeName(parent, sourceFile); + } + + return undefined; +} + +function collectTypeOrmListenerBindings(sourceFile) { + const importedNames = new Set(); + const namespaceNames = new Set(); + + for (const statement of sourceFile.statements) { + if ( + !ts.isImportDeclaration(statement) || + !ts.isStringLiteral(statement.moduleSpecifier) || + statement.moduleSpecifier.text !== 'typeorm' || + statement.importClause?.isTypeOnly + ) { + continue; + } + + const namedBindings = statement.importClause?.namedBindings; + if (ts.isNamespaceImport(namedBindings)) { + namespaceNames.add(namedBindings.name.text); + continue; + } + if (!ts.isNamedImports(namedBindings)) continue; + + for (const element of namedBindings.elements) { + const importedName = element.propertyName?.text || element.name.text; + if ( + !element.isTypeOnly && + TYPEORM_ENTITY_LISTENER_DECORATORS.has(importedName) + ) { + importedNames.add(element.name.text); + } + } + } + + return { + importedNames, + namespaceNames, + }; +} + +function getTypeOrmListenerDecoratorName(node, bindings) { + if (!ts.canHaveDecorators(node)) return undefined; + + for (const decorator of ts.getDecorators(node) || []) { + const expression = ts.isCallExpression(decorator.expression) + ? decorator.expression.expression + : decorator.expression; + + if ( + ts.isIdentifier(expression) && + bindings.importedNames.has(expression.text) + ) { + return expression.text; + } + + if ( + ts.isPropertyAccessExpression(expression) && + ts.isIdentifier(expression.expression) && + bindings.namespaceNames.has(expression.expression.text) && + TYPEORM_ENTITY_LISTENER_DECORATORS.has(expression.name.text) + ) { + return expression.name.text; + } + } + + return undefined; +} + +function maskNonNaturalLanguage(text) { + return text + .replace(/```[\s\S]*?```/gu, (code) => code.replace(/[^\r\n]/gu, '\0')) + .replace(/`[^`\r\n]*`/gu, '\0') + .replace(/\{@(?:code|link|linkcode|linkplain)\s+[^}]+\}/giu, '\0') + .replace(/\b(?:https?|ftp):\/\/[^\s<>{}|\\^`[\]]+/giu, '\0') + .replace(/\b[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}\b/gu, '\0') + .replace(/\bv?\d+(?:\.\d+){1,}\b/gu, '\0') + .replace(/<[^>\r\n]+>/gu, '\0') + .replace(/^[\s*-]+/u, '') + .trim(); +} + +function splitDescriptionSentences(text) { + return text + .split(/(?:[。!?;]+|[.!?;]+(?=\s|$))/u) + .map((sentence) => sentence.trim()) + .filter((sentence) => sentence.replaceAll('\0', '').trim()); +} + +function isStrongTechnicalWord(word) { + if (/\d/u.test(word)) return true; + return ( + word === word.toUpperCase() || + /[_$]/u.test(word) || + /[A-Z]/u.test(word.slice(1)) + ); +} + +function isTitleCaseWord(word) { + return /^[A-Z][a-z]+(?:-[A-Z]?[a-z]+)*$/u.test(word); +} + +function isLeadingDescriptionVerb(word) { + const lower = word.toLowerCase(); + const candidates = new Set([lower]); + + if (lower.endsWith('ies')) { + candidates.add(`${lower.slice(0, -3)}y`); + } + if (lower.endsWith('ing')) { + const stem = lower.slice(0, -3); + candidates.add(stem); + candidates.add(`${stem}e`); + if (stem.at(-1) === stem.at(-2)) { + candidates.add(stem.slice(0, -1)); + } + } + if (lower.endsWith('ed')) { + const stem = lower.slice(0, -2); + candidates.add(stem); + candidates.add(`${stem}e`); + } + if (lower.endsWith('es')) { + candidates.add(lower.slice(0, -2)); + candidates.add(lower.slice(0, -1)); + } else if (lower.endsWith('s')) { + candidates.add(lower.slice(0, -1)); + } + + return [...candidates].some((candidate) => + LEADING_DESCRIPTION_VERBS.has(candidate), + ); +} + +function containsEnglishClause(sentence) { + const chineseCount = [...sentence].filter((character) => + CHINESE_CHARACTER_PATTERN.test(character), + ).length; + + for (const region of sentence.split(/[\p{Script=Han}\0]+/u)) { + const words = [ + ...region.matchAll(/[A-Za-z][A-Za-z0-9]*(?:[-'][A-Za-z0-9]+)*/gu), + ].map((match) => match[0]); + const ordinaryWords = words.filter((word) => !isStrongTechnicalWord(word)); + if (ordinaryWords.length === 0) continue; + + if ( + ordinaryWords.length >= 2 && + isLeadingDescriptionVerb(ordinaryWords[0]) + ) { + return true; + } + + const isTitleCaseTechnicalPhrase = words.every( + (word) => isStrongTechnicalWord(word) || isTitleCaseWord(word), + ); + if (isTitleCaseTechnicalPhrase) continue; + + const hasFunctionWord = ordinaryWords.some((word) => + ENGLISH_FUNCTION_WORDS.has(word.toLowerCase()), + ); + + if ( + (ordinaryWords.length >= 2 && hasFunctionWord) || + (ordinaryWords.length >= 4 && ordinaryWords.length > chineseCount) + ) { + return true; + } + } + + return false; +} + +function isBareTypePayload(text) { + const payload = text.trim(); + if (!payload || payload.startsWith('-')) return false; + + const rootName = payload.match(/^[A-Za-z_$][A-Za-z0-9_$]*/u)?.[0]; + if ( + !rootName || + (!BUILTIN_TYPE_NAMES.has(rootName) && !/^[A-Z]/u.test(rootName)) + ) { + return false; + } + + const sourceFile = ts.createSourceFile( + 'jsdoc-type.ts', + `type JSDocPayload = ${payload};`, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS, + ); + return sourceFile.parseDiagnostics.length === 0; +} + +function stripLeadingTypeExpression(text) { + const trimmedText = text.trimStart(); + if (!trimmedText.startsWith('{')) return trimmedText; + + let depth = 0; + for (let index = 0; index < trimmedText.length; index += 1) { + const character = trimmedText[index]; + if (character === '{') { + depth += 1; + } else if (character === '}') { + depth -= 1; + if (depth === 0) { + return trimmedText.slice(index + 1).trimStart(); + } + } + } + + return trimmedText; +} + +function getTagDescription(tagName, rawValue) { + if (CODE_ONLY_TAGS.has(tagName)) return undefined; + + if (STRUCTURAL_TAGS.has(tagName) || tagName === 'see') { + const separatorIndex = rawValue.indexOf(' - '); + return separatorIndex === -1 + ? undefined + : rawValue.slice(separatorIndex + 3).trim() || undefined; + } + + let value = stripLeadingTypeExpression(rawValue); + if (PARAMETER_TAGS.has(tagName)) { + value = value + .replace(/^(?:\[[^\]]+\]|[^\s-]+)\s*/u, '') + .replace(/^-\s*/u, ''); + } else if (BARE_TYPE_TAGS.has(tagName) && isBareTypePayload(value)) { + return undefined; + } + + return value.trim() || undefined; +} + +function collectDescriptionUnits(rawComment) { + const units = []; + const bodyLines = rawComment + .replace(/^\/\*\*/u, '') + .replace(/\*\/$/u, '') + .split(/\r\n|\r|\n/u) + .map((line) => line.replace(/^\s*\*\s?/u, '').trimEnd()); + let current = { + label: '主说明', + lines: [], + startLineOffset: 0, + tagName: undefined, + }; + let insideFence = false; + + function flushCurrent() { + const firstContentLine = current.lines.findIndex( + (line) => line.trim() !== '', + ); + if (firstContentLine === -1) return; + + const rawValue = current.lines.slice(firstContentLine).join('\n').trim(); + if (!rawValue) return; + + const lineOffset = current.startLineOffset + firstContentLine; + if (!current.tagName) { + units.push({ + label: current.label, + lineOffset, + text: rawValue, + }); + return; + } + + const description = getTagDescription(current.tagName, rawValue); + if (description) { + units.push({ + label: current.label, + lineOffset, + text: description, + }); + } + } + + for (let lineOffset = 0; lineOffset < bodyLines.length; lineOffset += 1) { + const line = bodyLines[lineOffset]; + const fenceCount = line.match(/```/gu)?.length || 0; + + if (insideFence) { + current.lines.push(line); + if (fenceCount % 2 === 1) insideFence = false; + continue; + } + if (fenceCount > 0) { + current.lines.push(line); + if (fenceCount % 2 === 1) insideFence = true; + continue; + } + if (!current.tagName && !line.trim()) { + flushCurrent(); + current = { + label: '主说明', + lines: [], + startLineOffset: lineOffset + 1, + tagName: undefined, + }; + continue; + } + + const tagMatch = line.match(/^@([A-Za-z][\w-]*)(?:\s(.*))?$/u); + if (tagMatch) { + if (current.tagName === 'example' && /^[A-Z]/u.test(tagMatch[1])) { + continue; + } + + flushCurrent(); + const tagName = tagMatch[1].toLowerCase(); + current = { + label: `@${tagName}`, + lines: CODE_ONLY_TAGS.has(tagName) ? [] : [tagMatch[2] || ''], + startLineOffset: lineOffset, + tagName, + }; + continue; + } + + if (!CODE_ONLY_TAGS.has(current.tagName)) { + current.lines.push(line); + } + } + flushCurrent(); + + return units; +} + +function findDescriptionIssue(rawComment) { + const units = collectDescriptionUnits(rawComment); + + for (const unit of units) { + const maskedText = maskNonNaturalLanguage(unit.text); + const unitHasChinese = CHINESE_CHARACTER_PATTERN.test(maskedText); + const lines = maskedText.split(/\r\n|\r|\n/u); + + for (let lineOffset = 0; lineOffset < lines.length; lineOffset += 1) { + const sentences = splitDescriptionSentences(lines[lineOffset]); + + for (const sentence of sentences) { + const hasChinese = CHINESE_CHARACTER_PATTERN.test(sentence); + const hasEnglishClause = containsEnglishClause(sentence); + + if (!hasChinese) { + if (unitHasChinese && !hasEnglishClause) continue; + return { + label: unit.label, + lineOffset: unit.lineOffset + lineOffset, + reason: `${unit.label} 描述必须使用中文`, + }; + } + + if (hasEnglishClause) { + return { + label: unit.label, + lineOffset: unit.lineOffset + lineOffset, + reason: `${unit.label} 描述包含完整英文说明,必须改为中文`, + }; + } + } + } + } + + return undefined; +} + +function classifyOwner(node, sourceFile, typeOrmBindings) { if (!node) { return { code: 'invalid-target', @@ -177,12 +708,28 @@ function classifyOwner(node, sourceFile) { } const ownerName = getNodeName(node, sourceFile); - if (ownerName === 'setup' || LIFECYCLE_ENTRY_NAMES.has(ownerName)) { + const typeOrmListenerName = getTypeOrmListenerDecoratorName( + node, + typeOrmBindings, + ); + const roleName = ts.isFunctionExpression(node) + ? getFunctionExpressionRole(node, sourceFile) + : ts.isFunctionDeclaration(node) + ? undefined + : ownerName; + const isSetupEntry = roleName === 'setup'; + const lifecycleEntryName = + roleName && LIFECYCLE_ENTRY_NAMES.has(roleName) ? roleName : undefined; + + if (isSetupEntry || lifecycleEntryName || typeOrmListenerName) { + const entryName = typeOrmListenerName + ? `@${typeOrmListenerName}` + : lifecycleEntryName || roleName; return { code: 'forbidden-entry', ownerKind: ts.SyntaxKind[node.kind], ownerName, - reason: `${ownerName} 是禁止使用 JSDoc 的 setup 或生命周期入口`, + reason: `${entryName} 是禁止使用 JSDoc 的 setup 或生命周期入口`, }; } @@ -292,6 +839,7 @@ function analyzeScriptBlock(block) { ); const ownedDocs = collectOwnedJsDocs(sourceFile); const rawDocs = collectRawJsDocs(sourceFile); + const typeOrmBindings = collectTypeOrmListenerBindings(sourceFile); const issues = []; const removals = []; let retainedCount = 0; @@ -299,9 +847,10 @@ function analyzeScriptBlock(block) { for (const rawDoc of rawDocs) { const start = rawDoc.start; const end = rawDoc.end; - const text = block.sourceText.slice(start, end); - const owner = ownedDocs.get(`${start}:${end}`)?.owner; - const classification = classifyOwner(owner, sourceFile); + const rawComment = block.sourceText.slice(start, end); + const ownedDoc = ownedDocs.get(`${start}:${end}`); + const owner = ownedDoc?.owner; + const classification = classifyOwner(owner, sourceFile, typeOrmBindings); const position = sourceFile.getLineAndCharacterOfPosition(start); const common = { column: position.character + 1, @@ -326,11 +875,14 @@ function analyzeScriptBlock(block) { continue; } - if (!CHINESE_CHARACTER_PATTERN.test(text)) { + const descriptionIssue = findDescriptionIssue(rawComment); + if (descriptionIssue) { issues.push({ ...common, code: 'missing-chinese-description', - reason: 'JSDoc 描述必须整体以中文为主,不能是纯英文或仅标签', + descriptionLineOffset: descriptionIssue.lineOffset, + descriptionUnit: descriptionIssue.label, + reason: descriptionIssue.reason, }); removals.push({ end: common.end, @@ -385,6 +937,10 @@ export function validateSourceText(filePath, sourceText) { } function expandStandaloneRemoval(sourceText, removal) { + const lineTerminator = + sourceText + .slice(removal.start, removal.end) + .match(LINE_TERMINATOR_PATTERN)?.[0] || ''; const lineStart = sourceText.lastIndexOf('\n', removal.start - 1) + 1; const nextLineBreak = sourceText.indexOf('\n', removal.end); const lineEnd = nextLineBreak === -1 ? sourceText.length : nextLineBreak + 1; @@ -397,6 +953,7 @@ function expandStandaloneRemoval(sourceText, removal) { if (before.trim() === '' && after.trim() === '') { return { end: lineEnd, + replacement: '', start: lineStart, }; } @@ -405,11 +962,15 @@ function expandStandaloneRemoval(sourceText, removal) { if (horizontalWhitespace) { return { end: removal.end, + replacement: lineTerminator, start: removal.start - horizontalWhitespace.length, }; } - return removal; + return { + ...removal, + replacement: lineTerminator, + }; } function mergeRemovals(removals) { @@ -420,6 +981,7 @@ function mergeRemovals(removals) { const previous = merged.at(-1); if (previous && removal.start <= previous.end) { previous.end = Math.max(previous.end, removal.end); + previous.replacement ||= removal.replacement; } else { merged.push({ ...removal }); } @@ -440,6 +1002,7 @@ export function fixSourceText(filePath, sourceText) { for (const removal of removals.toReversed()) { fixedSourceText = fixedSourceText.slice(0, removal.start) + + removal.replacement + fixedSourceText.slice(removal.end); } @@ -458,9 +1021,23 @@ function tokenizeScript(filePath, sourceText, scriptKind) { true, scriptKind, ); - return getLeafTokens(sourceFile) - .filter((node) => node.kind !== ts.SyntaxKind.EndOfFileToken) - .map((node) => `${node.kind}:${node.getText(sourceFile)}`); + const tokens = getLeafTokens(sourceFile).filter( + (node) => node.kind !== ts.SyntaxKind.EndOfFileToken, + ); + + return tokens.map((node, index) => { + const previous = tokens[index - 1]; + const hasLineTerminator = + previous && + LINE_TERMINATOR_PATTERN.test( + sourceText.slice(previous.end, node.getStart(sourceFile)), + ); + const tokenText = node + .getText(sourceFile) + .replaceAll('\r\n', '\n') + .replaceAll('\r', '\n'); + return `${hasLineTerminator ? 'line-break' : 'same-line'}:${node.kind}:${tokenText}`; + }); } export function getTokenSignature(filePath, sourceText) { @@ -485,7 +1062,7 @@ function splitNullDelimited(output) { return output.toString('utf8').split('\0').filter(Boolean); } -function listCurrentSourceFiles(repoRoot) { +function listWorkingTreeSourceFiles(repoRoot) { const output = execFileSync( 'git', [ @@ -506,6 +1083,27 @@ function listCurrentSourceFiles(repoRoot) { return splitNullDelimited(output).filter(isSupportedSourceFile).sort(); } +function listIndexSourceFiles(repoRoot) { + const output = execFileSync( + 'git', + ['ls-files', '--cached', '-z', '--', ...SOURCE_PATHSPECS], + { + cwd: repoRoot, + encoding: 'buffer', + }, + ); + + return splitNullDelimited(output).filter(isSupportedSourceFile).sort(); +} + +function readIndexSourceFile(repoRoot, filePath) { + return execFileSync('git', ['show', `:${filePath}`], { + cwd: repoRoot, + encoding: 'utf8', + maxBuffer: 64 * 1024 * 1024, + }); +} + function listRefSourceFiles(repoRoot, ref) { const output = execFileSync( 'git', @@ -539,6 +1137,7 @@ function compareRepositoryTokens( repoRoot, ref, currentFiles, + readCurrentSource, allowedTokenChanges, ) { const currentFileSet = new Set(currentFiles); @@ -550,11 +1149,10 @@ function compareRepositoryTokens( let comparedTokens = 0; for (const filePath of refFiles) { - const absolutePath = path.resolve(repoRoot, filePath); - if (!currentFileSet.has(filePath) || !existsSync(absolutePath)) { + if (!currentFileSet.has(filePath)) { mismatches.push({ filePath, - reason: '基线文件在当前工作树中缺失', + reason: '基线文件在当前代码快照中缺失', }); continue; } @@ -564,7 +1162,7 @@ function compareRepositoryTokens( encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, }); - const afterSource = readFileSync(absolutePath, 'utf8'); + const afterSource = readCurrentSource(filePath); const beforeTokens = getTokenSignature(filePath, beforeSource); const afterTokens = getTokenSignature(filePath, afterSource); const difference = findFirstTokenDifference(beforeTokens, afterTokens); @@ -649,8 +1247,18 @@ function runRepositoryCheck({ compareRef, fix, repoRoot, + staged, }) { - const files = listCurrentSourceFiles(repoRoot); + if (fix && staged) { + throw new Error('--fix 不能与 --staged 同时使用'); + } + + const files = staged + ? listIndexSourceFiles(repoRoot) + : listWorkingTreeSourceFiles(repoRoot); + const readCurrentSource = staged + ? (filePath) => readIndexSourceFile(repoRoot, filePath) + : (filePath) => readFileSync(path.resolve(repoRoot, filePath), 'utf8'); const issues = []; const totals = { forbiddenEntryCount: 0, @@ -663,15 +1271,14 @@ function runRepositoryCheck({ let removedCount = 0; for (const filePath of files) { - const absolutePath = path.resolve(repoRoot, filePath); - const sourceText = readFileSync(absolutePath, 'utf8'); + const sourceText = readCurrentSource(filePath); if (fix) { const result = fixSourceText(filePath, sourceText); addStats(totals, result.stats); removedCount += result.removedCount; if (result.sourceText !== sourceText) { - writeFileSync(absolutePath, result.sourceText); + writeFileSync(path.resolve(repoRoot, filePath), result.sourceText); changedFiles += 1; } continue; @@ -683,7 +1290,13 @@ function runRepositoryCheck({ } const comparison = compareRef - ? compareRepositoryTokens(repoRoot, compareRef, files, allowedTokenChanges) + ? compareRepositoryTokens( + repoRoot, + compareRef, + files, + readCurrentSource, + allowedTokenChanges, + ) : undefined; const summary = { changedFiles, @@ -692,6 +1305,7 @@ function runRepositoryCheck({ issues: issues.length, mode: fix ? 'fix' : 'check', removedCount, + sourceMode: staged ? 'index' : 'worktree', ...totals, }; @@ -722,6 +1336,7 @@ function main() { compareRef: parseOption(args, '--compare-ref'), fix: args.includes('--fix'), repoRoot, + staged: args.includes('--staged'), }); } diff --git a/scripts/jsdoc/check-jsdoc-policy.test.mjs b/scripts/jsdoc/check-jsdoc-policy.test.mjs index ba80f97..14795bd 100644 --- a/scripts/jsdoc/check-jsdoc-policy.test.mjs +++ b/scripts/jsdoc/check-jsdoc-policy.test.mjs @@ -1,5 +1,10 @@ import assert from 'node:assert/strict'; +import { execFileSync, spawnSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; import test from 'node:test'; +import { fileURLToPath } from 'node:url'; import { fixSourceText, @@ -7,12 +12,100 @@ import { validateSourceText, } from './check-jsdoc-policy.mjs'; +const CHECKER_PATH = fileURLToPath( + new URL('./check-jsdoc-policy.mjs', import.meta.url), +); +const HOOK_CHECK_PATH = fileURLToPath( + new URL('../husky-fast-check.mjs', import.meta.url), +); + function issueCodes(sourceText, filePath = 'fixture.ts') { return validateSourceText(filePath, sourceText).issues.map( (issue) => issue.code, ); } +function createIsolatedGitEnvironment(sourceEnvironment = process.env) { + const environment = Object.fromEntries( + Object.entries(sourceEnvironment).filter( + ([name]) => !name.startsWith('GIT_'), + ), + ); + + return { + ...environment, + GIT_CONFIG_GLOBAL: '/dev/null', + GIT_CONFIG_NOSYSTEM: '1', + }; +} + +function executeGit( + repositoryPath, + args, + { environment = process.env, gitEnvironment = {} } = {}, +) { + return execFileSync('git', args, { + cwd: repositoryPath, + encoding: 'utf8', + env: { + ...createIsolatedGitEnvironment(environment), + ...gitEnvironment, + }, + }); +} + +function createFixtureRepository(sourceEnvironment = process.env) { + const repositoryPath = mkdtempSync(path.join(tmpdir(), 'kt-jsdoc-policy-')); + const validSource = '/** 解析输入。 */\nfunction parseInput() {}\n'; + + executeGit(repositoryPath, ['init', '--quiet'], { + environment: sourceEnvironment, + }); + executeGit(repositoryPath, ['config', 'user.email', 'codex@example.test'], { + environment: sourceEnvironment, + }); + executeGit(repositoryPath, ['config', 'user.name', 'Codex Test'], { + environment: sourceEnvironment, + }); + writeFileSync(path.join(repositoryPath, 'fixture.ts'), validSource); + writeFileSync( + path.join(repositoryPath, 'deleted.ts'), + '/** 读取输入。 */\nfunction readInput() {}\n', + ); + executeGit(repositoryPath, ['add', 'fixture.ts', 'deleted.ts'], { + environment: sourceEnvironment, + }); + executeGit( + repositoryPath, + ['-c', 'commit.gpgSign=false', 'commit', '--quiet', '-m', 'fixture'], + { environment: sourceEnvironment }, + ); + + return { + repositoryPath, + validSource, + }; +} + +function runChecker( + repositoryPath, + args, + { gitEnvironment = {}, sourceEnvironment = process.env } = {}, +) { + return spawnSync( + process.execPath, + [CHECKER_PATH, '--repo-root', repositoryPath, ...args], + { + cwd: repositoryPath, + encoding: 'utf8', + env: { + ...createIsolatedGitEnvironment(sourceEnvironment), + ...gitEnvironment, + }, + }, + ); +} + test('accepts Chinese JSDoc on explicitly named functions and methods', () => { const sourceText = ` /** 计算输入值。 */ @@ -77,6 +170,404 @@ class Example { ]); }); +test('rejects every imported TypeORM entity listener decorator', () => { + const listenerDecorators = [ + 'AfterLoad', + 'BeforeInsert', + 'AfterInsert', + 'BeforeUpdate', + 'AfterUpdate', + 'BeforeRemove', + 'AfterRemove', + 'BeforeSoftRemove', + 'AfterSoftRemove', + 'BeforeRecover', + 'AfterRecover', + ]; + const methods = listenerDecorators + .map( + (decoratorName, index) => ` + /** 处理实体监听事件。 */ + @${decoratorName}() + handleEntityEvent${index}() {} +`, + ) + .join(''); + const sourceText = ` +import { ${listenerDecorators.join(', ')} } from 'typeorm'; + +class Example { +${methods} +} +`; + + assert.deepEqual( + issueCodes(sourceText), + listenerDecorators.map(() => 'forbidden-entry'), + ); +}); + +test('recognizes aliased and namespace TypeORM listeners without rejecting ordinary same-name methods', () => { + const decoratedSource = ` +import { BeforeInsert as PersistEntity } from 'typeorm'; +import * as TypeOrm from 'typeorm'; + +class EntityExample { + /** 持久化实体。 */ + @PersistEntity() + persist() {} + + /** 加载实体。 */ + @TypeOrm.AfterLoad() + hydrate() {} +} +`; + const ordinarySource = ` +class OrdinaryExample { + /** 执行业务前置处理。 */ + beforeInsert() {} + + /** 执行业务加载处理。 */ + afterLoad() {} +} +`; + + assert.deepEqual(issueCodes(decoratedSource), [ + 'forbidden-entry', + 'forbidden-entry', + ]); + assert.deepEqual(issueCodes(ordinarySource), []); +}); + +test('rejects English prose in each main or tag description unit', () => { + const fixtures = [ + ` +/** 参数 Parse the entire incoming HTTP payload before dispatch. */ +function parsePayload() {} +`, + ` +/** + * 清理登录运行态。 + * @param options - Cleanup switches that may revisit provisional rows. + * @returns 清理结果。 + */ +function cleanupRuntime(options) {} +`, + ` +/** + * 格式化计数。 + * @param value - 原始计数。 + * @returns Counter text such as \`7890\` or \`12.3万\`. + */ +function formatCounter(value) {} +`, + ` +/** + * Parse the entire incoming payload before dispatch. + * @example + * const 参数 = readPayload(); + */ +function readPayload() {} +`, + ` +/** + * 中文说明。 + * Asynchronously handle the login process. + */ +function handleLogin() {} +`, + ` +/** 中文引子。 Determine whether there is permission from the user role. */ +function determinePermission() {} +`, + ` +/** 中文,the payload is returned by the API. */ +function returnPayload() {} +`, + ` +/** 中 Returns payload. */ +function readShortPayload() {} +`, + ` +/** Get machine-readable API runtime health。 */ +function getRuntimeHealth() {} +`, + ` +/** + * 查询结果。 + * @returns payload + */ +function readResult() {} +`, + ` +/** + * 查询字段。 + * @param fields record + */ +function readFields(fields) {} +`, + ]; + + for (const fixture of fixtures) { + assert.deepEqual(issueCodes(fixture), ['missing-chinese-description']); + } + + const tagIssue = validateSourceText('fixture.ts', fixtures[1]).issues[0]; + assert.equal(tagIssue.line, 2); + assert.match(tagIssue.reason, /@param/u); +}); + +test('allows masked code, URLs, identifiers, and necessary technical terms inside Chinese units', () => { + const fixtures = [ + ` +/** + * 解析 HTTP payload,并把 userId 写入 MySQL DTO。 + * @param payload - HTTP payload 输入。 + * @returns MySQL DTO 结果。 + * @see https://example.test/payload + * @example + * const 参数 = \`HTTP payload\`; + */ +function parsePayload(payload) {} +`, + ` +/** + * 解析请求: + * HTTP payload + * 并返回 DTO。 + */ +function parseTechnicalContinuation() {} +`, + ` +/** 设置 Dict Decode Cache。 */ +function setDictDecodeCache() {} +`, + ` +/** 创建 Markdown code fence 到 Argon 代码块 DOM 的 rehype 转换插件。 */ +function createMarkdownPlugin() {} +`, + ` +/** + * 读取文件。 + * @returns Blob + */ +function readBlob() {} +`, + ` +/** + * 请求文件。 + * @returns RequestResponse + */ +function requestBlob() {} +`, + ` +/** + * 查询角色。 + * @param roles + */ +function readRoles(roles) {} +`, + ]; + + for (const fixture of fixtures) { + assert.deepEqual(issueCodes(fixture), []); + } +}); + +test('treats inline code and structural tags as neutral while validating custom and marker tag prose', () => { + const validSource = ` +/** + * {@code 中文变量} + * @custom 中文扩展说明。 + * @public + * @author Jane Doe + * @type {Record} + * @returns RequestResponse + */ +function authenticate() {} +`; + const invalidCustomSource = ` +/** + * 执行载荷检查。 + * @custom payload {@code 中文变量} + */ +function checkPayload() {} +`; + const invalidMarkerSource = ` +/** + * 执行公开接口检查。 + * @public Public API access is enabled. + */ +function checkPublicApi() {} +`; + + assert.deepEqual(issueCodes(validSource), []); + + const customIssue = validateSourceText('fixture.ts', invalidCustomSource) + .issues[0]; + assert.equal(customIssue?.code, 'missing-chinese-description'); + assert.match(customIssue?.reason || '', /@custom/u); + + const markerIssue = validateSourceText('fixture.ts', invalidMarkerSource) + .issues[0]; + assert.equal(markerIssue?.code, 'missing-chinese-description'); + assert.match(markerIssue?.reason || '', /@public/u); +}); + +test('validates prose after structural tag payloads', () => { + const validSource = ` +/** + * 创建选项类型。 + * @typedef {object} Options - 选项说明。 + * @extends {Base} - 基类说明。 + */ +function createOptions() {} +`; + const invalidSource = ` +/** + * 创建选项类型。 + * @typedef {object} Options - This is English prose. + */ +function createOptions() {} +`; + + assert.deepEqual(issueCodes(validSource), []); + + const issue = validateSourceText('fixture.ts', invalidSource).issues[0]; + assert.equal(issue?.code, 'missing-chinese-description'); + assert.match(issue?.reason || '', /@typedef/u); +}); + +test('does not parse fenced code or example decorators as description tags', () => { + const sourceText = ` +/** + * 展示装饰器示例。 + * \`\`\`ts + * @returns payload + * \`\`\` + * @example + * @Component + * class Demo {} + */ +function showDecoratorExample() {} +`; + + assert.deepEqual(issueCodes(sourceText), []); +}); + +test('does not share a Chinese anchor across main-description paragraphs', () => { + const invalidSource = ` +/** + * 中文说明。 + * + * Cache expires tomorrow. + */ +function readCache() {} +`; + const validSource = ` +/** + * 解析载荷。 + * HTTP payload + * 并返回结果。 + */ +function readPayload() {} +`; + + assert.deepEqual(issueCodes(invalidSource), ['missing-chinese-description']); + assert.deepEqual(issueCodes(validSource), []); +}); + +test('uses property roles for setup and lifecycle function expressions', () => { + const sourceText = ` +const component = { + mounted: /** 组件挂载入口。 */ function mountedHook() {}, + setup: /** 组件初始化入口。 */ function namedSetup() {}, + ['mounted']: /** 计算属性挂载入口。 */ function computedMountedHook() {}, + ['setup']: /** 计算属性初始化入口。 */ function computedSetup() {}, + updated: (/** 括号包裹的更新入口。 */ function updatedHook() {}), + task: /** 执行普通任务。 */ function namedTask() {}, + ['computedTask']: /** 执行普通计算属性任务。 */ function computedTask() {}, +}; + +/** 读取普通同名函数。 */ +function mounted() {} +`; + const issues = validateSourceText('fixture.ts', sourceText).issues; + + assert.deepEqual( + issues.map((issue) => issue.code), + [ + 'forbidden-entry', + 'forbidden-entry', + 'forbidden-entry', + 'forbidden-entry', + 'forbidden-entry', + ], + ); + assert.deepEqual( + issues.map(({ ownerName, reason }) => [ownerName, reason]), + [ + ['mountedHook', 'mounted 是禁止使用 JSDoc 的 setup 或生命周期入口'], + ['namedSetup', 'setup 是禁止使用 JSDoc 的 setup 或生命周期入口'], + [ + 'computedMountedHook', + 'mounted 是禁止使用 JSDoc 的 setup 或生命周期入口', + ], + ['computedSetup', 'setup 是禁止使用 JSDoc 的 setup 或生命周期入口'], + ['updatedHook', 'updated 是禁止使用 JSDoc 的 setup 或生命周期入口'], + ], + ); +}); + +test('allows ordinary same-name named functions outside entry roles', () => { + const sourceText = ` +/** 执行普通初始化任务。 */ +function setup() {} + +const setupTask = + /** 执行普通具名函数表达式。 */ + function setup() {}; + +const mountedTask = + /** 执行普通具名函数表达式。 */ + function mounted() {}; + +consume( + /** 执行普通具名回调。 */ + function onModuleInit() {}, +); +`; + + assert.deepEqual(issueCodes(sourceText), []); +}); + +test('uses class property roles for setup and lifecycle function expressions', () => { + const sourceText = ` +class ComponentFields { + setup = + /** 组件初始化类字段入口。 */ + function setupField() {}; + + static mounted = + /** 组件挂载类字段入口。 */ + function mountedField() {}; + + task = + /** 执行普通类字段任务。 */ + function setup() {}; +} +`; + const issues = validateSourceText('fixture.ts', sourceText).issues; + + assert.deepEqual( + issues.map(({ code, ownerName }) => [code, ownerName]), + [ + ['forbidden-entry', 'setupField'], + ['forbidden-entry', 'mountedField'], + ], + ); +}); + test('rejects JSDoc on forbidden declaration and expression targets', () => { const fixtures = [ ` @@ -213,3 +704,167 @@ function calculateResult() { beforeTokens, ); }); + +test('preserves LineTerminator-sensitive return semantics while fixing multiline JSDoc', () => { + const sourceText = ` +function readValue() { + return /** English-only + * comment. + */ 1; +} +`; + const unsafeSourceText = sourceText.replace( + '/** English-only\n * comment.\n */', + '', + ); + const beforeTokens = getTokenSignature('fixture.ts', sourceText); + const result = fixSourceText('fixture.ts', sourceText); + const evaluate = (text) => Function(`${text}\nreturn readValue();`)(); + + assert.notDeepEqual( + getTokenSignature('fixture.ts', unsafeSourceText), + beforeTokens, + ); + assert.equal(evaluate(sourceText), undefined); + assert.equal(evaluate(result.sourceText), undefined); + assert.match(result.sourceText, /return[^\S\r\n]*\r?\n[^\S\r\n]*1/u); + assert.deepEqual( + getTokenSignature('fixture.ts', result.sourceText), + beforeTokens, + ); +}); + +test('treats LF and CRLF as the same LineTerminator boundary', () => { + const sourceText = ` +function readValue() { + return + \`line one +line two\`; +} +`; + + assert.deepEqual( + getTokenSignature('fixture.ts', sourceText.replaceAll('\n', '\r\n')), + getTokenSignature('fixture.ts', sourceText), + ); +}); + +test('staged mode reads additions, modifications, and deletions from the index', () => { + const { repositoryPath, validSource } = createFixtureRepository(); + const invalidSource = + '/** Parse the entire incoming payload. */\nfunction parseInput() {}\n'; + + try { + writeFileSync(path.join(repositoryPath, 'fixture.ts'), invalidSource); + executeGit(repositoryPath, ['add', 'fixture.ts']); + writeFileSync(path.join(repositoryPath, 'fixture.ts'), validSource); + + const invalidIndexResult = runChecker(repositoryPath, ['--staged']); + assert.equal(invalidIndexResult.status, 1); + assert.match(invalidIndexResult.stderr, /missing-chinese-description/u); + + executeGit(repositoryPath, ['add', 'fixture.ts']); + writeFileSync(path.join(repositoryPath, 'fixture.ts'), invalidSource); + + const invalidWorktreeResult = runChecker(repositoryPath, ['--staged']); + assert.equal(invalidWorktreeResult.status, 0); + + writeFileSync(path.join(repositoryPath, 'added.ts'), invalidSource); + executeGit(repositoryPath, ['add', 'added.ts']); + writeFileSync( + path.join(repositoryPath, 'added.ts'), + '/** 解析新增输入。 */\nfunction parseAddedInput() {}\n', + ); + + const invalidAdditionResult = runChecker(repositoryPath, ['--staged']); + assert.equal(invalidAdditionResult.status, 1); + + executeGit(repositoryPath, ['add', 'added.ts']); + executeGit(repositoryPath, ['rm', '--quiet', 'deleted.ts']); + writeFileSync(path.join(repositoryPath, 'untracked.ts'), invalidSource); + + const deletionAndUntrackedResult = runChecker(repositoryPath, ['--staged']); + assert.equal(deletionAndUntrackedResult.status, 0); + assert.match(deletionAndUntrackedResult.stdout, /"sourceMode": "index"/u); + } finally { + rmSync(repositoryPath, { force: true, recursive: true }); + } +}); + +test('staged mode honors the active GIT_INDEX_FILE used by the commit', () => { + const { repositoryPath, validSource } = createFixtureRepository(); + const alternateIndexPath = path.join(repositoryPath, '.git', 'commit-index'); + const invalidSource = + '/** Parse the commit index payload. */\nfunction parseInput() {}\n'; + const alternateIndexEnvironment = { + GIT_INDEX_FILE: alternateIndexPath, + }; + + try { + executeGit(repositoryPath, ['read-tree', 'HEAD'], { + gitEnvironment: alternateIndexEnvironment, + }); + writeFileSync(path.join(repositoryPath, 'fixture.ts'), invalidSource); + executeGit(repositoryPath, ['add', 'fixture.ts'], { + gitEnvironment: alternateIndexEnvironment, + }); + writeFileSync(path.join(repositoryPath, 'fixture.ts'), validSource); + + assert.equal(runChecker(repositoryPath, ['--staged']).status, 0); + + const commitIndexResult = runChecker(repositoryPath, ['--staged'], { + gitEnvironment: alternateIndexEnvironment, + }); + assert.equal(commitIndexResult.status, 1); + assert.match(commitIndexResult.stderr, /missing-chinese-description/u); + } finally { + rmSync(repositoryPath, { force: true, recursive: true }); + } +}); + +test('fixture Git commands isolate hostile inherited GIT variables from the parent repository', () => { + const parentRepositoryPath = executeGit(process.cwd(), [ + 'rev-parse', + '--show-toplevel', + ]).trim(); + const parentGitDirectory = executeGit(parentRepositoryPath, [ + 'rev-parse', + '--absolute-git-dir', + ]).trim(); + const snapshotParent = () => ({ + config: readFileSync(path.join(parentGitDirectory, 'config'), 'utf8'), + head: executeGit(parentRepositoryPath, ['rev-parse', 'HEAD']).trim(), + headReference: readFileSync(path.join(parentGitDirectory, 'HEAD'), 'utf8'), + index: readFileSync(path.join(parentGitDirectory, 'index')).toString( + 'base64', + ), + status: executeGit(parentRepositoryPath, [ + 'status', + '--porcelain=v2', + '-z', + ]), + }); + const before = snapshotParent(); + const hostileEnvironment = { + ...process.env, + GIT_DIR: parentGitDirectory, + GIT_INDEX_FILE: path.join(parentGitDirectory, 'index'), + GIT_WORK_TREE: parentRepositoryPath, + }; + const { repositoryPath } = createFixtureRepository(hostileEnvironment); + + try { + assert.deepEqual(snapshotParent(), before); + } finally { + rmSync(repositoryPath, { force: true, recursive: true }); + } +}); + +test('pre-commit invokes the JSDoc checker in staged mode', () => { + const hookSource = readFileSync(HOOK_CHECK_PATH, 'utf8'); + + assert.match( + hookSource, + /run\(getPnpmCommand\(\), \['run', 'check:jsdoc', '--', '--staged'\]\)/u, + ); +}); diff --git a/src/modules/admin/identity/dept/admin-dept.entity.ts b/src/modules/admin/identity/dept/admin-dept.entity.ts index 34fd7e4..4d24aa0 100644 --- a/src/modules/admin/identity/dept/admin-dept.entity.ts +++ b/src/modules/admin/identity/dept/admin-dept.entity.ts @@ -47,9 +47,6 @@ export class AdminDept { }) updateTime: KtDateTime; - /** - * 创建 Admin 身份权限对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/admin/identity/menu/admin-menu.entity.ts b/src/modules/admin/identity/menu/admin-menu.entity.ts index edd4d7d..e8050b4 100644 --- a/src/modules/admin/identity/menu/admin-menu.entity.ts +++ b/src/modules/admin/identity/menu/admin-menu.entity.ts @@ -99,9 +99,6 @@ export class AdminMenu { @ManyToMany(() => AdminRole, (role) => role.menus) roles: AdminRole[]; - /** - * 创建 Admin 身份权限对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/admin/identity/role/admin-role.entity.ts b/src/modules/admin/identity/role/admin-role.entity.ts index da1f891..0be3417 100644 --- a/src/modules/admin/identity/role/admin-role.entity.ts +++ b/src/modules/admin/identity/role/admin-role.entity.ts @@ -74,9 +74,6 @@ export class AdminRole { @ManyToMany(() => AdminUser, (user) => user.roles) users: AdminUser[]; - /** - * 创建 Admin 身份权限对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/admin/identity/user/admin-user.entity.ts b/src/modules/admin/identity/user/admin-user.entity.ts index e10fe0d..883f676 100644 --- a/src/modules/admin/identity/user/admin-user.entity.ts +++ b/src/modules/admin/identity/user/admin-user.entity.ts @@ -107,9 +107,6 @@ export class AdminUser { }) dept?: AdminDept | null; - /** - * 创建 Admin 身份权限对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/admin/platform-config/component/component.entity.ts b/src/modules/admin/platform-config/component/component.entity.ts index 6020c12..da69ad8 100644 --- a/src/modules/admin/platform-config/component/component.entity.ts +++ b/src/modules/admin/platform-config/component/component.entity.ts @@ -83,18 +83,12 @@ export class Component { }) is_deleted: boolean; - /** - * 转换 Admin 平台配置输入。 - */ @AfterLoad() decodeDictKeys() { // 查询结果初始化完成后再翻译,避免构造/赋值阶段覆盖派生字段。 decodeDictKeys(this); } - /** - * 创建 Admin 平台配置对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/admin/platform-config/dict/admin-dict.entity.ts b/src/modules/admin/platform-config/dict/admin-dict.entity.ts index b3bddac..2d414c7 100644 --- a/src/modules/admin/platform-config/dict/admin-dict.entity.ts +++ b/src/modules/admin/platform-config/dict/admin-dict.entity.ts @@ -72,9 +72,6 @@ export class AdminDict { }) updateTime: KtDateTime; - /** - * 创建 Admin 平台配置对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/admin/platform-config/notice/admin-notice.entity.ts b/src/modules/admin/platform-config/notice/admin-notice.entity.ts index 24bddbd..99b5082 100644 --- a/src/modules/admin/platform-config/notice/admin-notice.entity.ts +++ b/src/modules/admin/platform-config/notice/admin-notice.entity.ts @@ -181,9 +181,6 @@ export class AdminNotice { }) lastSeenAt?: KtDateTime; - /** - * 创建 Admin 平台配置对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/blog/infrastructure/persistence/blog-article.entity.ts b/src/modules/blog/infrastructure/persistence/blog-article.entity.ts index 6237ae9..1afc5f2 100644 --- a/src/modules/blog/infrastructure/persistence/blog-article.entity.ts +++ b/src/modules/blog/infrastructure/persistence/blog-article.entity.ts @@ -147,9 +147,6 @@ export class BlogArticle { @ApiPropertyOptional() excerptText?: string; - /** - * 创建 博客内容对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/blog/infrastructure/persistence/blog-term.entity.ts b/src/modules/blog/infrastructure/persistence/blog-term.entity.ts index f25921c..b407d07 100644 --- a/src/modules/blog/infrastructure/persistence/blog-term.entity.ts +++ b/src/modules/blog/infrastructure/persistence/blog-term.entity.ts @@ -69,9 +69,6 @@ export class BlogTerm { @ApiPropertyOptional() parent?: string; - /** - * 创建 博客内容对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/core/infrastructure/persistence/account/qqbot-account-ability.entity.ts b/src/modules/qqbot/core/infrastructure/persistence/account/qqbot-account-ability.entity.ts index cc2a8e3..8754a55 100644 --- a/src/modules/qqbot/core/infrastructure/persistence/account/qqbot-account-ability.entity.ts +++ b/src/modules/qqbot/core/infrastructure/persistence/account/qqbot-account-ability.entity.ts @@ -37,9 +37,6 @@ export class QqbotAccountAbility { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 核心对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/core/infrastructure/persistence/account/qqbot-account.entity.ts b/src/modules/qqbot/core/infrastructure/persistence/account/qqbot-account.entity.ts index ac79396..745818c 100644 --- a/src/modules/qqbot/core/infrastructure/persistence/account/qqbot-account.entity.ts +++ b/src/modules/qqbot/core/infrastructure/persistence/account/qqbot-account.entity.ts @@ -99,9 +99,6 @@ export class QqbotAccount { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 核心对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/core/infrastructure/persistence/command/qqbot-command-log.entity.ts b/src/modules/qqbot/core/infrastructure/persistence/command/qqbot-command-log.entity.ts index 7f91d5f..0b59810 100644 --- a/src/modules/qqbot/core/infrastructure/persistence/command/qqbot-command-log.entity.ts +++ b/src/modules/qqbot/core/infrastructure/persistence/command/qqbot-command-log.entity.ts @@ -63,9 +63,6 @@ export class QqbotCommandLog { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 核心对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/core/infrastructure/persistence/command/qqbot-command.entity.ts b/src/modules/qqbot/core/infrastructure/persistence/command/qqbot-command.entity.ts index 05f1a5a..b1fd219 100644 --- a/src/modules/qqbot/core/infrastructure/persistence/command/qqbot-command.entity.ts +++ b/src/modules/qqbot/core/infrastructure/persistence/command/qqbot-command.entity.ts @@ -93,9 +93,6 @@ export class QqbotCommand { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 核心对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/core/infrastructure/persistence/config/qqbot-config.entity.ts b/src/modules/qqbot/core/infrastructure/persistence/config/qqbot-config.entity.ts index 86ffb62..d79c8be 100644 --- a/src/modules/qqbot/core/infrastructure/persistence/config/qqbot-config.entity.ts +++ b/src/modules/qqbot/core/infrastructure/persistence/config/qqbot-config.entity.ts @@ -26,9 +26,6 @@ export class QqbotConfig { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 核心对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/core/infrastructure/persistence/dedupe/qqbot-dedupe.entity.ts b/src/modules/qqbot/core/infrastructure/persistence/dedupe/qqbot-dedupe.entity.ts index 6c97a70..29f7938 100644 --- a/src/modules/qqbot/core/infrastructure/persistence/dedupe/qqbot-dedupe.entity.ts +++ b/src/modules/qqbot/core/infrastructure/persistence/dedupe/qqbot-dedupe.entity.ts @@ -28,9 +28,6 @@ export class QqbotDedupe { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 核心对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/core/infrastructure/persistence/message/qqbot-conversation.entity.ts b/src/modules/qqbot/core/infrastructure/persistence/message/qqbot-conversation.entity.ts index dbccc22..abe9287 100644 --- a/src/modules/qqbot/core/infrastructure/persistence/message/qqbot-conversation.entity.ts +++ b/src/modules/qqbot/core/infrastructure/persistence/message/qqbot-conversation.entity.ts @@ -57,9 +57,6 @@ export class QqbotConversation { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 核心对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/core/infrastructure/persistence/message/qqbot-message.entity.ts b/src/modules/qqbot/core/infrastructure/persistence/message/qqbot-message.entity.ts index 19eb5f0..1e480ff 100644 --- a/src/modules/qqbot/core/infrastructure/persistence/message/qqbot-message.entity.ts +++ b/src/modules/qqbot/core/infrastructure/persistence/message/qqbot-message.entity.ts @@ -67,9 +67,6 @@ export class QqbotMessage { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 核心对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/core/infrastructure/persistence/permission/qqbot-allowlist.entity.ts b/src/modules/qqbot/core/infrastructure/persistence/permission/qqbot-allowlist.entity.ts index b900d15..bee6010 100644 --- a/src/modules/qqbot/core/infrastructure/persistence/permission/qqbot-allowlist.entity.ts +++ b/src/modules/qqbot/core/infrastructure/persistence/permission/qqbot-allowlist.entity.ts @@ -42,9 +42,6 @@ export class QqbotAllowlist { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 核心对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/core/infrastructure/persistence/permission/qqbot-blocklist.entity.ts b/src/modules/qqbot/core/infrastructure/persistence/permission/qqbot-blocklist.entity.ts index 908e066..23664d0 100644 --- a/src/modules/qqbot/core/infrastructure/persistence/permission/qqbot-blocklist.entity.ts +++ b/src/modules/qqbot/core/infrastructure/persistence/permission/qqbot-blocklist.entity.ts @@ -42,9 +42,6 @@ export class QqbotBlocklist { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 核心对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/core/infrastructure/persistence/rule/qqbot-rule.entity.ts b/src/modules/qqbot/core/infrastructure/persistence/rule/qqbot-rule.entity.ts index c4693fe..3a9f463 100644 --- a/src/modules/qqbot/core/infrastructure/persistence/rule/qqbot-rule.entity.ts +++ b/src/modules/qqbot/core/infrastructure/persistence/rule/qqbot-rule.entity.ts @@ -60,9 +60,6 @@ export class QqbotRule { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 核心对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/core/infrastructure/persistence/send/qqbot-send-log.entity.ts b/src/modules/qqbot/core/infrastructure/persistence/send/qqbot-send-log.entity.ts index a494e6a..f994e11 100644 --- a/src/modules/qqbot/core/infrastructure/persistence/send/qqbot-send-log.entity.ts +++ b/src/modules/qqbot/core/infrastructure/persistence/send/qqbot-send-log.entity.ts @@ -55,9 +55,6 @@ export class QqbotSendLog { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 核心对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/napcat/application/login/qqbot-napcat-login.service.ts b/src/modules/qqbot/napcat/application/login/qqbot-napcat-login.service.ts index c14c14a..82f204e 100644 --- a/src/modules/qqbot/napcat/application/login/qqbot-napcat-login.service.ts +++ b/src/modules/qqbot/napcat/application/login/qqbot-napcat-login.service.ts @@ -2195,11 +2195,6 @@ export class QqbotNapcatLoginService { } } - /** - * 清理 NapCat 登录运行态状态。 - * @param container - container 输入;使用 `id` 字段生成结果。 - * @param options - Cleanup switches; create-login cleanup may revisit already-deleted provisional rows after async startup races. - */ private async cleanupRuntimeContainer( container: QqbotNapcatRuntime, options: { includeDeletedCreateContainer?: boolean } = {}, diff --git a/src/modules/qqbot/napcat/infrastructure/integration/container/napcat-docker-device-options.ts b/src/modules/qqbot/napcat/infrastructure/integration/container/napcat-docker-device-options.ts index ec0c17d..ae130c7 100644 --- a/src/modules/qqbot/napcat/infrastructure/integration/container/napcat-docker-device-options.ts +++ b/src/modules/qqbot/napcat/infrastructure/integration/container/napcat-docker-device-options.ts @@ -15,11 +15,6 @@ export type NapcatDockerDeviceOptions = { runFlags: string[]; }; -/** - * 执行 NapCat 登录运行态流程。 - * @param identity - Persisted device identity row that supplies stable directory, hostname, machine-id, and MAC values for Docker. - * @returns Docker option bundle used by remote create scripts. - */ export function toNapcatDockerDeviceOptions( identity: Pick< NapcatDeviceIdentity, diff --git a/src/modules/qqbot/napcat/infrastructure/persistence/napcat-account-binding.entity.ts b/src/modules/qqbot/napcat/infrastructure/persistence/napcat-account-binding.entity.ts index 7cc6173..47a70ba 100644 --- a/src/modules/qqbot/napcat/infrastructure/persistence/napcat-account-binding.entity.ts +++ b/src/modules/qqbot/napcat/infrastructure/persistence/napcat-account-binding.entity.ts @@ -55,9 +55,6 @@ export class NapcatAccountBinding { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 NapCat 登录运行态对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/napcat/infrastructure/persistence/napcat-container.entity.ts b/src/modules/qqbot/napcat/infrastructure/persistence/napcat-container.entity.ts index dd70d13..b9a4849 100644 --- a/src/modules/qqbot/napcat/infrastructure/persistence/napcat-container.entity.ts +++ b/src/modules/qqbot/napcat/infrastructure/persistence/napcat-container.entity.ts @@ -80,9 +80,6 @@ export class NapcatContainer { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 NapCat 登录运行态对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/napcat/infrastructure/persistence/napcat-device-identity.entity.ts b/src/modules/qqbot/napcat/infrastructure/persistence/napcat-device-identity.entity.ts index 78f725d..909c63e 100644 --- a/src/modules/qqbot/napcat/infrastructure/persistence/napcat-device-identity.entity.ts +++ b/src/modules/qqbot/napcat/infrastructure/persistence/napcat-device-identity.entity.ts @@ -65,9 +65,6 @@ export class NapcatDeviceIdentity { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 NapCat 登录运行态对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/napcat/infrastructure/persistence/napcat-login-challenge.entity.ts b/src/modules/qqbot/napcat/infrastructure/persistence/napcat-login-challenge.entity.ts index 97d2a47..b4afe0b 100644 --- a/src/modules/qqbot/napcat/infrastructure/persistence/napcat-login-challenge.entity.ts +++ b/src/modules/qqbot/napcat/infrastructure/persistence/napcat-login-challenge.entity.ts @@ -54,9 +54,6 @@ export class NapcatLoginChallengeEntity { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 NapCat 登录运行态对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/napcat/infrastructure/persistence/napcat-login-session.entity.ts b/src/modules/qqbot/napcat/infrastructure/persistence/napcat-login-session.entity.ts index 4eff880..1cf3b04 100644 --- a/src/modules/qqbot/napcat/infrastructure/persistence/napcat-login-session.entity.ts +++ b/src/modules/qqbot/napcat/infrastructure/persistence/napcat-login-session.entity.ts @@ -63,9 +63,6 @@ export class NapcatLoginSession { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 NapCat 登录运行态对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/napcat/infrastructure/persistence/napcat-runtime-cleanup.entity.ts b/src/modules/qqbot/napcat/infrastructure/persistence/napcat-runtime-cleanup.entity.ts index 2a7aad6..16c81fe 100644 --- a/src/modules/qqbot/napcat/infrastructure/persistence/napcat-runtime-cleanup.entity.ts +++ b/src/modules/qqbot/napcat/infrastructure/persistence/napcat-runtime-cleanup.entity.ts @@ -37,9 +37,6 @@ export class NapcatRuntimeCleanup { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 NapCat 登录运行态对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/plugin-platform/infrastructure/persistence/plugin-platform.entities.ts b/src/modules/qqbot/plugin-platform/infrastructure/persistence/plugin-platform.entities.ts index 7011194..c02fbc1 100644 --- a/src/modules/qqbot/plugin-platform/infrastructure/persistence/plugin-platform.entities.ts +++ b/src/modules/qqbot/plugin-platform/infrastructure/persistence/plugin-platform.entities.ts @@ -65,9 +65,6 @@ export class QqbotPlugin { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 插件平台对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); @@ -95,9 +92,6 @@ export class QqbotPluginVersion { @KtCreateDateColumn({ name: 'create_time' }) createTime: KtDateTime; - /** - * 创建 QQBot 插件平台对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); @@ -131,9 +125,6 @@ export class QqbotPluginInstallation { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 插件平台对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); @@ -166,9 +157,6 @@ export class QqbotPluginOperation { @KtCreateDateColumn({ name: 'create_time' }) createTime: KtDateTime; - /** - * 创建 QQBot 插件平台对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); @@ -198,9 +186,6 @@ export class QqbotPluginEventHandler { @KtCreateDateColumn({ name: 'create_time' }) createTime: KtDateTime; - /** - * 创建 QQBot 插件平台对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); @@ -227,9 +212,6 @@ export class QqbotPluginAccountBinding { @KtCreateDateColumn({ name: 'create_time' }) createTime: KtDateTime; - /** - * 创建 QQBot 插件平台对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); @@ -257,9 +239,6 @@ export class QqbotPluginConfig { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 插件平台对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); @@ -287,9 +266,6 @@ export class QqbotPluginAsset { @KtCreateDateColumn({ name: 'create_time' }) createTime: KtDateTime; - /** - * 创建 QQBot 插件平台对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); @@ -325,9 +301,6 @@ export class QqbotPluginRuntimeEvent { @KtCreateDateColumn({ name: 'create_time' }) createTime: KtDateTime; - /** - * 创建 QQBot 插件平台对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); @@ -402,9 +375,6 @@ export class QqbotPluginTask { @KtUpdateDateColumn({ name: 'update_time' }) updateTime: KtDateTime; - /** - * 创建 QQBot 插件平台对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); @@ -458,9 +428,6 @@ export class QqbotPluginTaskRun { @KtCreateDateColumn({ name: 'create_time' }) createTime: KtDateTime; - /** - * 创建 QQBot 插件平台对象或配置。 - */ @BeforeInsert() createId() { ensureSnowflakeId(this); diff --git a/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-reply-formatter.ts b/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-reply-formatter.ts index 4f63f63..a750019 100644 --- a/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-reply-formatter.ts +++ b/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-reply-formatter.ts @@ -43,11 +43,6 @@ function buildCanonicalBilibiliVideoUrl(video: BilibiliVideoInfo) { return `https://www.bilibili.com/video/${videoId}`; } -/** - * Formats Bilibili stat counters using compact Chinese units. - * @param value - Raw counter from the Bilibili video API response. - * @returns Counter text such as `7890` or `12.3万`. - */ function formatBilibiliStat(value: number) { const normalized = Math.max(0, Math.floor(value || 0)); if (normalized < 10000) return `${normalized}`; diff --git a/src/modules/qqbot/plugins/fflogs/src/infrastructure/integration/fflogs-client.ts b/src/modules/qqbot/plugins/fflogs/src/infrastructure/integration/fflogs-client.ts index 77347af..d2efba0 100644 --- a/src/modules/qqbot/plugins/fflogs/src/infrastructure/integration/fflogs-client.ts +++ b/src/modules/qqbot/plugins/fflogs/src/infrastructure/integration/fflogs-client.ts @@ -248,11 +248,6 @@ export class FflogsClient { return (value: string) => resolved.get(value) || null; } - /** - * 解析Known World。 - * @param value - FF14 server, region, data-center, or path token parsed from FFLogs command text. - * @returns Legacy host resolution when available, otherwise package-owned dictionary fallback for generic workers. - */ async resolveKnownWorld(value: string) { if (this.host.resolveKnownWorld) { return this.host.resolveKnownWorld(value); diff --git a/src/runtime/health/runtime-health.controller.ts b/src/runtime/health/runtime-health.controller.ts index f006842..324b174 100644 --- a/src/runtime/health/runtime-health.controller.ts +++ b/src/runtime/health/runtime-health.controller.ts @@ -8,10 +8,6 @@ import type { RuntimeHealthReport } from './runtime-health.types'; export class RuntimeHealthController { constructor(private readonly runtimeHealthService: RuntimeHealthService) {} - /** - * Get machine-readable API runtime health。 - * @returns 运行态健康检查查询结果。 - */ @Get('runtime') @ApiOperation({ summary: 'Get machine-readable API runtime health' }) getRuntimeHealth(): RuntimeHealthReport {