fix: 收紧API JSDoc门禁
This commit is contained in:
parent
ffd3f503c5
commit
de9dd56bb6
@ -90,7 +90,7 @@ function getStagedFiles() {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
run(getPnpmCommand(), ['run', 'check:jsdoc']);
|
run(getPnpmCommand(), ['run', 'check:jsdoc', '--', '--staged']);
|
||||||
|
|
||||||
const files = getStagedFiles().filter((file) => {
|
const files = getStagedFiles().filter((file) => {
|
||||||
if (!existsSync(file)) return false;
|
if (!existsSync(file)) return false;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { execFileSync } from 'node:child_process';
|
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 path from 'node:path';
|
||||||
import { pathToFileURL } from 'node:url';
|
import { pathToFileURL } from 'node:url';
|
||||||
|
|
||||||
@ -17,35 +17,168 @@ const SOURCE_EXTENSIONS = new Set([
|
|||||||
]);
|
]);
|
||||||
const SOURCE_PATHSPECS = ['*.cjs', '*.js', '*.mjs', '*.ts', '*.tsx', '*.vue'];
|
const SOURCE_PATHSPECS = ['*.cjs', '*.js', '*.mjs', '*.ts', '*.tsx', '*.vue'];
|
||||||
const CHINESE_CHARACTER_PATTERN = /\p{Script=Han}/u;
|
const CHINESE_CHARACTER_PATTERN = /\p{Script=Han}/u;
|
||||||
|
const LINE_TERMINATOR_PATTERN = /\r\n|\r|\n/u;
|
||||||
const VUE_SCRIPT_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/giu;
|
const VUE_SCRIPT_PATTERN = /<script\b([^>]*)>([\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([
|
const LIFECYCLE_ENTRY_NAMES = new Set([
|
||||||
'activated',
|
'activated',
|
||||||
'afterInit',
|
'afterInit',
|
||||||
'afterInsert',
|
|
||||||
'afterLoad',
|
|
||||||
'afterQuery',
|
'afterQuery',
|
||||||
'afterRecover',
|
|
||||||
'afterRemove',
|
|
||||||
'afterSoftRemove',
|
|
||||||
'afterTransactionCommit',
|
'afterTransactionCommit',
|
||||||
'afterTransactionRollback',
|
'afterTransactionRollback',
|
||||||
'afterTransactionStart',
|
'afterTransactionStart',
|
||||||
'afterUpdate',
|
|
||||||
'beforeApplicationShutdown',
|
'beforeApplicationShutdown',
|
||||||
'beforeCreate',
|
'beforeCreate',
|
||||||
'beforeDestroy',
|
'beforeDestroy',
|
||||||
'beforeInsert',
|
|
||||||
'beforeMount',
|
'beforeMount',
|
||||||
'beforeModuleDestroy',
|
'beforeModuleDestroy',
|
||||||
'beforeQuery',
|
'beforeQuery',
|
||||||
'beforeRecover',
|
|
||||||
'beforeRemove',
|
|
||||||
'beforeSoftRemove',
|
|
||||||
'beforeTransactionCommit',
|
'beforeTransactionCommit',
|
||||||
'beforeTransactionRollback',
|
'beforeTransactionRollback',
|
||||||
'beforeTransactionStart',
|
'beforeTransactionStart',
|
||||||
'beforeUnmount',
|
'beforeUnmount',
|
||||||
'beforeUpdate',
|
|
||||||
'componentDidCatch',
|
'componentDidCatch',
|
||||||
'componentDidMount',
|
'componentDidMount',
|
||||||
'componentDidUpdate',
|
'componentDidUpdate',
|
||||||
@ -148,10 +281,408 @@ function getNodeName(node, sourceFile) {
|
|||||||
return node.name.text;
|
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);
|
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) {
|
if (!node) {
|
||||||
return {
|
return {
|
||||||
code: 'invalid-target',
|
code: 'invalid-target',
|
||||||
@ -177,12 +708,28 @@ function classifyOwner(node, sourceFile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ownerName = getNodeName(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 {
|
return {
|
||||||
code: 'forbidden-entry',
|
code: 'forbidden-entry',
|
||||||
ownerKind: ts.SyntaxKind[node.kind],
|
ownerKind: ts.SyntaxKind[node.kind],
|
||||||
ownerName,
|
ownerName,
|
||||||
reason: `${ownerName} 是禁止使用 JSDoc 的 setup 或生命周期入口`,
|
reason: `${entryName} 是禁止使用 JSDoc 的 setup 或生命周期入口`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,6 +839,7 @@ function analyzeScriptBlock(block) {
|
|||||||
);
|
);
|
||||||
const ownedDocs = collectOwnedJsDocs(sourceFile);
|
const ownedDocs = collectOwnedJsDocs(sourceFile);
|
||||||
const rawDocs = collectRawJsDocs(sourceFile);
|
const rawDocs = collectRawJsDocs(sourceFile);
|
||||||
|
const typeOrmBindings = collectTypeOrmListenerBindings(sourceFile);
|
||||||
const issues = [];
|
const issues = [];
|
||||||
const removals = [];
|
const removals = [];
|
||||||
let retainedCount = 0;
|
let retainedCount = 0;
|
||||||
@ -299,9 +847,10 @@ function analyzeScriptBlock(block) {
|
|||||||
for (const rawDoc of rawDocs) {
|
for (const rawDoc of rawDocs) {
|
||||||
const start = rawDoc.start;
|
const start = rawDoc.start;
|
||||||
const end = rawDoc.end;
|
const end = rawDoc.end;
|
||||||
const text = block.sourceText.slice(start, end);
|
const rawComment = block.sourceText.slice(start, end);
|
||||||
const owner = ownedDocs.get(`${start}:${end}`)?.owner;
|
const ownedDoc = ownedDocs.get(`${start}:${end}`);
|
||||||
const classification = classifyOwner(owner, sourceFile);
|
const owner = ownedDoc?.owner;
|
||||||
|
const classification = classifyOwner(owner, sourceFile, typeOrmBindings);
|
||||||
const position = sourceFile.getLineAndCharacterOfPosition(start);
|
const position = sourceFile.getLineAndCharacterOfPosition(start);
|
||||||
const common = {
|
const common = {
|
||||||
column: position.character + 1,
|
column: position.character + 1,
|
||||||
@ -326,11 +875,14 @@ function analyzeScriptBlock(block) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!CHINESE_CHARACTER_PATTERN.test(text)) {
|
const descriptionIssue = findDescriptionIssue(rawComment);
|
||||||
|
if (descriptionIssue) {
|
||||||
issues.push({
|
issues.push({
|
||||||
...common,
|
...common,
|
||||||
code: 'missing-chinese-description',
|
code: 'missing-chinese-description',
|
||||||
reason: 'JSDoc 描述必须整体以中文为主,不能是纯英文或仅标签',
|
descriptionLineOffset: descriptionIssue.lineOffset,
|
||||||
|
descriptionUnit: descriptionIssue.label,
|
||||||
|
reason: descriptionIssue.reason,
|
||||||
});
|
});
|
||||||
removals.push({
|
removals.push({
|
||||||
end: common.end,
|
end: common.end,
|
||||||
@ -385,6 +937,10 @@ export function validateSourceText(filePath, sourceText) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function expandStandaloneRemoval(sourceText, removal) {
|
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 lineStart = sourceText.lastIndexOf('\n', removal.start - 1) + 1;
|
||||||
const nextLineBreak = sourceText.indexOf('\n', removal.end);
|
const nextLineBreak = sourceText.indexOf('\n', removal.end);
|
||||||
const lineEnd = nextLineBreak === -1 ? sourceText.length : nextLineBreak + 1;
|
const lineEnd = nextLineBreak === -1 ? sourceText.length : nextLineBreak + 1;
|
||||||
@ -397,6 +953,7 @@ function expandStandaloneRemoval(sourceText, removal) {
|
|||||||
if (before.trim() === '' && after.trim() === '') {
|
if (before.trim() === '' && after.trim() === '') {
|
||||||
return {
|
return {
|
||||||
end: lineEnd,
|
end: lineEnd,
|
||||||
|
replacement: '',
|
||||||
start: lineStart,
|
start: lineStart,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -405,11 +962,15 @@ function expandStandaloneRemoval(sourceText, removal) {
|
|||||||
if (horizontalWhitespace) {
|
if (horizontalWhitespace) {
|
||||||
return {
|
return {
|
||||||
end: removal.end,
|
end: removal.end,
|
||||||
|
replacement: lineTerminator,
|
||||||
start: removal.start - horizontalWhitespace.length,
|
start: removal.start - horizontalWhitespace.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return removal;
|
return {
|
||||||
|
...removal,
|
||||||
|
replacement: lineTerminator,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeRemovals(removals) {
|
function mergeRemovals(removals) {
|
||||||
@ -420,6 +981,7 @@ function mergeRemovals(removals) {
|
|||||||
const previous = merged.at(-1);
|
const previous = merged.at(-1);
|
||||||
if (previous && removal.start <= previous.end) {
|
if (previous && removal.start <= previous.end) {
|
||||||
previous.end = Math.max(previous.end, removal.end);
|
previous.end = Math.max(previous.end, removal.end);
|
||||||
|
previous.replacement ||= removal.replacement;
|
||||||
} else {
|
} else {
|
||||||
merged.push({ ...removal });
|
merged.push({ ...removal });
|
||||||
}
|
}
|
||||||
@ -440,6 +1002,7 @@ export function fixSourceText(filePath, sourceText) {
|
|||||||
for (const removal of removals.toReversed()) {
|
for (const removal of removals.toReversed()) {
|
||||||
fixedSourceText =
|
fixedSourceText =
|
||||||
fixedSourceText.slice(0, removal.start) +
|
fixedSourceText.slice(0, removal.start) +
|
||||||
|
removal.replacement +
|
||||||
fixedSourceText.slice(removal.end);
|
fixedSourceText.slice(removal.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,9 +1021,23 @@ function tokenizeScript(filePath, sourceText, scriptKind) {
|
|||||||
true,
|
true,
|
||||||
scriptKind,
|
scriptKind,
|
||||||
);
|
);
|
||||||
return getLeafTokens(sourceFile)
|
const tokens = getLeafTokens(sourceFile).filter(
|
||||||
.filter((node) => node.kind !== ts.SyntaxKind.EndOfFileToken)
|
(node) => node.kind !== ts.SyntaxKind.EndOfFileToken,
|
||||||
.map((node) => `${node.kind}:${node.getText(sourceFile)}`);
|
);
|
||||||
|
|
||||||
|
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) {
|
export function getTokenSignature(filePath, sourceText) {
|
||||||
@ -485,7 +1062,7 @@ function splitNullDelimited(output) {
|
|||||||
return output.toString('utf8').split('\0').filter(Boolean);
|
return output.toString('utf8').split('\0').filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
function listCurrentSourceFiles(repoRoot) {
|
function listWorkingTreeSourceFiles(repoRoot) {
|
||||||
const output = execFileSync(
|
const output = execFileSync(
|
||||||
'git',
|
'git',
|
||||||
[
|
[
|
||||||
@ -506,6 +1083,27 @@ function listCurrentSourceFiles(repoRoot) {
|
|||||||
return splitNullDelimited(output).filter(isSupportedSourceFile).sort();
|
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) {
|
function listRefSourceFiles(repoRoot, ref) {
|
||||||
const output = execFileSync(
|
const output = execFileSync(
|
||||||
'git',
|
'git',
|
||||||
@ -539,6 +1137,7 @@ function compareRepositoryTokens(
|
|||||||
repoRoot,
|
repoRoot,
|
||||||
ref,
|
ref,
|
||||||
currentFiles,
|
currentFiles,
|
||||||
|
readCurrentSource,
|
||||||
allowedTokenChanges,
|
allowedTokenChanges,
|
||||||
) {
|
) {
|
||||||
const currentFileSet = new Set(currentFiles);
|
const currentFileSet = new Set(currentFiles);
|
||||||
@ -550,11 +1149,10 @@ function compareRepositoryTokens(
|
|||||||
let comparedTokens = 0;
|
let comparedTokens = 0;
|
||||||
|
|
||||||
for (const filePath of refFiles) {
|
for (const filePath of refFiles) {
|
||||||
const absolutePath = path.resolve(repoRoot, filePath);
|
if (!currentFileSet.has(filePath)) {
|
||||||
if (!currentFileSet.has(filePath) || !existsSync(absolutePath)) {
|
|
||||||
mismatches.push({
|
mismatches.push({
|
||||||
filePath,
|
filePath,
|
||||||
reason: '基线文件在当前工作树中缺失',
|
reason: '基线文件在当前代码快照中缺失',
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -564,7 +1162,7 @@ function compareRepositoryTokens(
|
|||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
maxBuffer: 64 * 1024 * 1024,
|
maxBuffer: 64 * 1024 * 1024,
|
||||||
});
|
});
|
||||||
const afterSource = readFileSync(absolutePath, 'utf8');
|
const afterSource = readCurrentSource(filePath);
|
||||||
const beforeTokens = getTokenSignature(filePath, beforeSource);
|
const beforeTokens = getTokenSignature(filePath, beforeSource);
|
||||||
const afterTokens = getTokenSignature(filePath, afterSource);
|
const afterTokens = getTokenSignature(filePath, afterSource);
|
||||||
const difference = findFirstTokenDifference(beforeTokens, afterTokens);
|
const difference = findFirstTokenDifference(beforeTokens, afterTokens);
|
||||||
@ -649,8 +1247,18 @@ function runRepositoryCheck({
|
|||||||
compareRef,
|
compareRef,
|
||||||
fix,
|
fix,
|
||||||
repoRoot,
|
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 issues = [];
|
||||||
const totals = {
|
const totals = {
|
||||||
forbiddenEntryCount: 0,
|
forbiddenEntryCount: 0,
|
||||||
@ -663,15 +1271,14 @@ function runRepositoryCheck({
|
|||||||
let removedCount = 0;
|
let removedCount = 0;
|
||||||
|
|
||||||
for (const filePath of files) {
|
for (const filePath of files) {
|
||||||
const absolutePath = path.resolve(repoRoot, filePath);
|
const sourceText = readCurrentSource(filePath);
|
||||||
const sourceText = readFileSync(absolutePath, 'utf8');
|
|
||||||
|
|
||||||
if (fix) {
|
if (fix) {
|
||||||
const result = fixSourceText(filePath, sourceText);
|
const result = fixSourceText(filePath, sourceText);
|
||||||
addStats(totals, result.stats);
|
addStats(totals, result.stats);
|
||||||
removedCount += result.removedCount;
|
removedCount += result.removedCount;
|
||||||
if (result.sourceText !== sourceText) {
|
if (result.sourceText !== sourceText) {
|
||||||
writeFileSync(absolutePath, result.sourceText);
|
writeFileSync(path.resolve(repoRoot, filePath), result.sourceText);
|
||||||
changedFiles += 1;
|
changedFiles += 1;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@ -683,7 +1290,13 @@ function runRepositoryCheck({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const comparison = compareRef
|
const comparison = compareRef
|
||||||
? compareRepositoryTokens(repoRoot, compareRef, files, allowedTokenChanges)
|
? compareRepositoryTokens(
|
||||||
|
repoRoot,
|
||||||
|
compareRef,
|
||||||
|
files,
|
||||||
|
readCurrentSource,
|
||||||
|
allowedTokenChanges,
|
||||||
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
const summary = {
|
const summary = {
|
||||||
changedFiles,
|
changedFiles,
|
||||||
@ -692,6 +1305,7 @@ function runRepositoryCheck({
|
|||||||
issues: issues.length,
|
issues: issues.length,
|
||||||
mode: fix ? 'fix' : 'check',
|
mode: fix ? 'fix' : 'check',
|
||||||
removedCount,
|
removedCount,
|
||||||
|
sourceMode: staged ? 'index' : 'worktree',
|
||||||
...totals,
|
...totals,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -722,6 +1336,7 @@ function main() {
|
|||||||
compareRef: parseOption(args, '--compare-ref'),
|
compareRef: parseOption(args, '--compare-ref'),
|
||||||
fix: args.includes('--fix'),
|
fix: args.includes('--fix'),
|
||||||
repoRoot,
|
repoRoot,
|
||||||
|
staged: args.includes('--staged'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import assert from 'node:assert/strict';
|
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 test from 'node:test';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fixSourceText,
|
fixSourceText,
|
||||||
@ -7,12 +12,100 @@ import {
|
|||||||
validateSourceText,
|
validateSourceText,
|
||||||
} from './check-jsdoc-policy.mjs';
|
} 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') {
|
function issueCodes(sourceText, filePath = 'fixture.ts') {
|
||||||
return validateSourceText(filePath, sourceText).issues.map(
|
return validateSourceText(filePath, sourceText).issues.map(
|
||||||
(issue) => issue.code,
|
(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', () => {
|
test('accepts Chinese JSDoc on explicitly named functions and methods', () => {
|
||||||
const sourceText = `
|
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<Blob>
|
||||||
|
*/
|
||||||
|
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<string, unknown>}
|
||||||
|
* @returns RequestResponse<Blob>
|
||||||
|
*/
|
||||||
|
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', () => {
|
test('rejects JSDoc on forbidden declaration and expression targets', () => {
|
||||||
const fixtures = [
|
const fixtures = [
|
||||||
`
|
`
|
||||||
@ -213,3 +704,167 @@ function calculateResult() {
|
|||||||
beforeTokens,
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@ -47,9 +47,6 @@ export class AdminDept {
|
|||||||
})
|
})
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 Admin 身份权限对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -99,9 +99,6 @@ export class AdminMenu {
|
|||||||
@ManyToMany(() => AdminRole, (role) => role.menus)
|
@ManyToMany(() => AdminRole, (role) => role.menus)
|
||||||
roles: AdminRole[];
|
roles: AdminRole[];
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 Admin 身份权限对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -74,9 +74,6 @@ export class AdminRole {
|
|||||||
@ManyToMany(() => AdminUser, (user) => user.roles)
|
@ManyToMany(() => AdminUser, (user) => user.roles)
|
||||||
users: AdminUser[];
|
users: AdminUser[];
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 Admin 身份权限对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -107,9 +107,6 @@ export class AdminUser {
|
|||||||
})
|
})
|
||||||
dept?: AdminDept | null;
|
dept?: AdminDept | null;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 Admin 身份权限对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -83,18 +83,12 @@ export class Component {
|
|||||||
})
|
})
|
||||||
is_deleted: boolean;
|
is_deleted: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* 转换 Admin 平台配置输入。
|
|
||||||
*/
|
|
||||||
@AfterLoad()
|
@AfterLoad()
|
||||||
decodeDictKeys() {
|
decodeDictKeys() {
|
||||||
// 查询结果初始化完成后再翻译,避免构造/赋值阶段覆盖派生字段。
|
// 查询结果初始化完成后再翻译,避免构造/赋值阶段覆盖派生字段。
|
||||||
decodeDictKeys(this);
|
decodeDictKeys(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 Admin 平台配置对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -72,9 +72,6 @@ export class AdminDict {
|
|||||||
})
|
})
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 Admin 平台配置对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -181,9 +181,6 @@ export class AdminNotice {
|
|||||||
})
|
})
|
||||||
lastSeenAt?: KtDateTime;
|
lastSeenAt?: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 Admin 平台配置对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -147,9 +147,6 @@ export class BlogArticle {
|
|||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
excerptText?: string;
|
excerptText?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 博客内容对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -69,9 +69,6 @@ export class BlogTerm {
|
|||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
parent?: string;
|
parent?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 博客内容对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -37,9 +37,6 @@ export class QqbotAccountAbility {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 核心对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -99,9 +99,6 @@ export class QqbotAccount {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 核心对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -63,9 +63,6 @@ export class QqbotCommandLog {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 核心对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -93,9 +93,6 @@ export class QqbotCommand {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 核心对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -26,9 +26,6 @@ export class QqbotConfig {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 核心对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -28,9 +28,6 @@ export class QqbotDedupe {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 核心对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -57,9 +57,6 @@ export class QqbotConversation {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 核心对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -67,9 +67,6 @@ export class QqbotMessage {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 核心对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -42,9 +42,6 @@ export class QqbotAllowlist {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 核心对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -42,9 +42,6 @@ export class QqbotBlocklist {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 核心对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -60,9 +60,6 @@ export class QqbotRule {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 核心对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -55,9 +55,6 @@ export class QqbotSendLog {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 核心对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -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(
|
private async cleanupRuntimeContainer(
|
||||||
container: QqbotNapcatRuntime,
|
container: QqbotNapcatRuntime,
|
||||||
options: { includeDeletedCreateContainer?: boolean } = {},
|
options: { includeDeletedCreateContainer?: boolean } = {},
|
||||||
|
|||||||
@ -15,11 +15,6 @@ export type NapcatDockerDeviceOptions = {
|
|||||||
runFlags: string[];
|
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(
|
export function toNapcatDockerDeviceOptions(
|
||||||
identity: Pick<
|
identity: Pick<
|
||||||
NapcatDeviceIdentity,
|
NapcatDeviceIdentity,
|
||||||
|
|||||||
@ -55,9 +55,6 @@ export class NapcatAccountBinding {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 NapCat 登录运行态对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -80,9 +80,6 @@ export class NapcatContainer {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 NapCat 登录运行态对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -65,9 +65,6 @@ export class NapcatDeviceIdentity {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 NapCat 登录运行态对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -54,9 +54,6 @@ export class NapcatLoginChallengeEntity {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 NapCat 登录运行态对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -63,9 +63,6 @@ export class NapcatLoginSession {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 NapCat 登录运行态对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -37,9 +37,6 @@ export class NapcatRuntimeCleanup {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 NapCat 登录运行态对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -65,9 +65,6 @@ export class QqbotPlugin {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 插件平台对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
@ -95,9 +92,6 @@ export class QqbotPluginVersion {
|
|||||||
@KtCreateDateColumn({ name: 'create_time' })
|
@KtCreateDateColumn({ name: 'create_time' })
|
||||||
createTime: KtDateTime;
|
createTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 插件平台对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
@ -131,9 +125,6 @@ export class QqbotPluginInstallation {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 插件平台对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
@ -166,9 +157,6 @@ export class QqbotPluginOperation {
|
|||||||
@KtCreateDateColumn({ name: 'create_time' })
|
@KtCreateDateColumn({ name: 'create_time' })
|
||||||
createTime: KtDateTime;
|
createTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 插件平台对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
@ -198,9 +186,6 @@ export class QqbotPluginEventHandler {
|
|||||||
@KtCreateDateColumn({ name: 'create_time' })
|
@KtCreateDateColumn({ name: 'create_time' })
|
||||||
createTime: KtDateTime;
|
createTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 插件平台对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
@ -227,9 +212,6 @@ export class QqbotPluginAccountBinding {
|
|||||||
@KtCreateDateColumn({ name: 'create_time' })
|
@KtCreateDateColumn({ name: 'create_time' })
|
||||||
createTime: KtDateTime;
|
createTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 插件平台对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
@ -257,9 +239,6 @@ export class QqbotPluginConfig {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 插件平台对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
@ -287,9 +266,6 @@ export class QqbotPluginAsset {
|
|||||||
@KtCreateDateColumn({ name: 'create_time' })
|
@KtCreateDateColumn({ name: 'create_time' })
|
||||||
createTime: KtDateTime;
|
createTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 插件平台对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
@ -325,9 +301,6 @@ export class QqbotPluginRuntimeEvent {
|
|||||||
@KtCreateDateColumn({ name: 'create_time' })
|
@KtCreateDateColumn({ name: 'create_time' })
|
||||||
createTime: KtDateTime;
|
createTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 插件平台对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
@ -402,9 +375,6 @@ export class QqbotPluginTask {
|
|||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
@KtUpdateDateColumn({ name: 'update_time' })
|
||||||
updateTime: KtDateTime;
|
updateTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 插件平台对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
@ -458,9 +428,6 @@ export class QqbotPluginTaskRun {
|
|||||||
@KtCreateDateColumn({ name: 'create_time' })
|
@KtCreateDateColumn({ name: 'create_time' })
|
||||||
createTime: KtDateTime;
|
createTime: KtDateTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 QQBot 插件平台对象或配置。
|
|
||||||
*/
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
createId() {
|
createId() {
|
||||||
ensureSnowflakeId(this);
|
ensureSnowflakeId(this);
|
||||||
|
|||||||
@ -43,11 +43,6 @@ function buildCanonicalBilibiliVideoUrl(video: BilibiliVideoInfo) {
|
|||||||
return `https://www.bilibili.com/video/${videoId}`;
|
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) {
|
function formatBilibiliStat(value: number) {
|
||||||
const normalized = Math.max(0, Math.floor(value || 0));
|
const normalized = Math.max(0, Math.floor(value || 0));
|
||||||
if (normalized < 10000) return `${normalized}`;
|
if (normalized < 10000) return `${normalized}`;
|
||||||
|
|||||||
@ -248,11 +248,6 @@ export class FflogsClient {
|
|||||||
return (value: string) => resolved.get(value) || null;
|
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) {
|
async resolveKnownWorld(value: string) {
|
||||||
if (this.host.resolveKnownWorld) {
|
if (this.host.resolveKnownWorld) {
|
||||||
return this.host.resolveKnownWorld(value);
|
return this.host.resolveKnownWorld(value);
|
||||||
|
|||||||
@ -8,10 +8,6 @@ import type { RuntimeHealthReport } from './runtime-health.types';
|
|||||||
export class RuntimeHealthController {
|
export class RuntimeHealthController {
|
||||||
constructor(private readonly runtimeHealthService: RuntimeHealthService) {}
|
constructor(private readonly runtimeHealthService: RuntimeHealthService) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get machine-readable API runtime health。
|
|
||||||
* @returns 运行态健康检查查询结果。
|
|
||||||
*/
|
|
||||||
@Get('runtime')
|
@Get('runtime')
|
||||||
@ApiOperation({ summary: 'Get machine-readable API runtime health' })
|
@ApiOperation({ summary: 'Get machine-readable API runtime health' })
|
||||||
getRuntimeHealth(): RuntimeHealthReport {
|
getRuntimeHealth(): RuntimeHealthReport {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user