fix: 收紧Admin JSDoc门禁

This commit is contained in:
sunlei 2026-07-25 11:00:15 +08:00
parent 523f96c3b2
commit 56ab9b9359
9 changed files with 937 additions and 61 deletions

View File

@ -106,12 +106,6 @@ export const useAuthStore = defineStore('auth', () => {
await router.push(target); await router.push(target);
} }
/**
*
* Asynchronously handle the login process
* @param params
* @param onSuccess
*/
async function authLogin( async function authLogin(
params: Recordable<any>, params: Recordable<any>,
onSuccess?: () => Promise<void> | void, onSuccess?: () => Promise<void> | void,

View File

@ -87,10 +87,6 @@ const formSchema = computed((): VbenFormSchema[] => {
}, },
]; ];
}); });
/**
* 异步处理登录操作
* Asynchronously handle the login process
*/
async function handleLogin() { async function handleLogin() {
loading.value = true; loading.value = true;
loading.value = false; loading.value = false;

View File

@ -64,7 +64,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;

View File

@ -11,12 +11,169 @@ import { parse as parseVueSfc } from 'vue/compiler-sfc';
const CODE_FILE_PATTERN = /\.(?:cjs|js|mjs|ts|tsx|vue)$/u; const CODE_FILE_PATTERN = /\.(?:cjs|js|mjs|ts|tsx|vue)$/u;
const HAN_CHARACTER_PATTERN = /\p{Script=Han}/u; const HAN_CHARACTER_PATTERN = /\p{Script=Han}/u;
const JSDOC_PREFIX = '/**'; const JSDOC_PREFIX = '/**';
const ENGLISH_FUNCTION_WORDS = new Set([
'a',
'after',
'an',
'and',
'are',
'as',
'at',
'be',
'before',
'by',
'for',
'from',
'if',
'in',
'into',
'is',
'of',
'on',
'or',
'otherwise',
'that',
'the',
'these',
'this',
'those',
'to',
'was',
'when',
'whether',
'with',
]);
const ENGLISH_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 DESCRIPTION_TAG_NAMES = new Set([
'arg',
'argument',
'deprecated',
'description',
'exception',
'param',
'prop',
'property',
'remarks',
'return',
'returns',
'see',
'summary',
'template',
'throws',
'todo',
'typeparam',
'yield',
'yields',
]);
const PARAMETER_TAG_NAMES = new Set([
'arg',
'argument',
'param',
'prop',
'property',
'template',
'typeparam',
]);
const RETURN_TAG_NAMES = new Set(['return', 'returns', 'yield', 'yields']);
const MARKER_TAG_NAMES = new Set([
'abstract',
'async',
'generator',
'inheritdoc',
'internal',
'override',
'private',
'protected',
'public',
'readonly',
'static',
]);
const STRUCTURAL_TAG_NAMES = 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 TYPE_ONLY_WORDS = new Set([
'any',
'bigint',
'boolean',
'never',
'null',
'number',
'object',
'string',
'symbol',
'undefined',
'unknown',
'void',
]);
const LIFECYCLE_METHOD_NAMES = new Set([ const LIFECYCLE_METHOD_NAMES = new Set([
'activated', 'activated',
'beforeCreate', 'beforeCreate',
'beforeDestroy', 'beforeDestroy',
'beforeMount', 'beforeMount',
'beforeRouteEnter',
'beforeRouteLeave',
'beforeRouteUpdate',
'beforeUnmount', 'beforeUnmount',
'beforeUpdate', 'beforeUpdate',
'componentDidCatch', 'componentDidCatch',
@ -30,6 +187,8 @@ const LIFECYCLE_METHOD_NAMES = new Set([
'deactivated', 'deactivated',
'destroyed', 'destroyed',
'errorCaptured', 'errorCaptured',
'getDerivedStateFromError',
'getDerivedStateFromProps',
'getSnapshotBeforeUpdate', 'getSnapshotBeforeUpdate',
'mounted', 'mounted',
'renderTracked', 'renderTracked',
@ -46,6 +205,52 @@ function getIsolatedGitEnvironment() {
); );
} }
function isPathInside(parentPath, candidatePath) {
const relativePath = path.relative(parentPath, candidatePath);
return (
relativePath === '' ||
(!relativePath.startsWith('..') && !path.isAbsolute(relativePath))
);
}
function getStagedGitEnvironment(rootDirectory) {
const environment = getIsolatedGitEnvironment();
const indexFile = process.env.GIT_INDEX_FILE;
if (!indexFile) {
return environment;
}
const gitDirectory = execFileSync(
'git',
['rev-parse', '--path-format=absolute', '--git-dir'],
{
cwd: rootDirectory,
encoding: 'utf8',
env: environment,
},
).trim();
const commonDirectory = execFileSync(
'git',
['rev-parse', '--path-format=absolute', '--git-common-dir'],
{
cwd: rootDirectory,
encoding: 'utf8',
env: environment,
},
).trim();
const absoluteIndexFile = path.resolve(rootDirectory, indexFile);
if (
isPathInside(gitDirectory, absoluteIndexFile) ||
isPathInside(commonDirectory, absoluteIndexFile)
) {
environment.GIT_INDEX_FILE = absoluteIndexFile;
}
return environment;
}
function getLineStarts(source) { function getLineStarts(source) {
const starts = [0]; const starts = [0];
@ -84,6 +289,300 @@ function getLineAndColumn(lineStarts, position) {
}; };
} }
function stripLeadingTypeExpression(value) {
const trimmedValue = value.trimStart();
if (!trimmedValue.startsWith('{')) {
return trimmedValue;
}
let depth = 0;
for (let index = 0; index < trimmedValue.length; index += 1) {
const character = trimmedValue[index];
if (character === '{') {
depth += 1;
} else if (character === '}') {
depth -= 1;
if (depth === 0) {
return trimmedValue.slice(index + 1).trimStart();
}
}
}
return trimmedValue;
}
function isTypeOnlyDescription(value) {
const normalizedValue = value.trim();
if (normalizedValue === '') {
return true;
}
const typeParts = normalizedValue.split(/\s*[|&]\s*/u);
return typeParts.every((part) => {
const arraylessPart = part.replaceAll(/\[\]$/gu, '');
if (TYPE_ONLY_WORDS.has(arraylessPart)) {
return true;
}
return /^[A-Z_$][\w$]*(?:<.+>)?$/u.test(arraylessPart);
});
}
function getTagDescription(tagName, rawValue) {
if (tagName === 'example') {
return null;
}
if (STRUCTURAL_TAG_NAMES.has(tagName)) {
const separatorIndex = rawValue.indexOf(' - ');
return separatorIndex === -1
? null
: rawValue.slice(separatorIndex + 3).trim() || null;
}
if (tagName === 'see') {
const separatorIndex = rawValue.indexOf(' - ');
return separatorIndex === -1
? null
: rawValue.slice(separatorIndex + 3).trim() || null;
}
if (!DESCRIPTION_TAG_NAMES.has(tagName) && !MARKER_TAG_NAMES.has(tagName)) {
return rawValue.trim() || null;
}
let value = stripLeadingTypeExpression(rawValue);
if (PARAMETER_TAG_NAMES.has(tagName)) {
value = value
.replace(/^(?:\[[^\]]+\]|[^\s-]+)\s*/u, '')
.replace(/^-\s*/u, '');
} else if (RETURN_TAG_NAMES.has(tagName) && isTypeOnlyDescription(value)) {
return null;
} else if (
(tagName === 'throws' || tagName === 'exception') &&
isTypeOnlyDescription(value)
) {
return null;
}
const description = value.trim();
return description === '' ? null : description;
}
function getJsdocDescriptionUnits(rawComment) {
const bodyLines = rawComment
.replace(/^\/\*\*/u, '')
.replace(/\*\/$/u, '')
.split(/\r\n|\r|\n/gu)
.map((line) => line.replace(/^\s*\*\s?/u, '').trimEnd());
const units = [];
let current = {
context: '主说明',
lines: [],
startLineOffset: 0,
tagName: null,
};
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 === null) {
units.push({ context: current.context, lineOffset, text: rawValue });
return;
}
const description = getTagDescription(current.tagName, rawValue);
if (description !== null) {
units.push({ context: current.context, lineOffset, text: description });
}
}
for (const [lineOffset, line] of bodyLines.entries()) {
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 === null && line.trim() === '') {
flushCurrent();
current = {
context: '主说明',
lines: [],
startLineOffset: lineOffset + 1,
tagName: null,
};
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 = {
context: `@${tagName}`,
lines: tagName === 'example' ? [] : [tagMatch[2] ?? ''],
startLineOffset: lineOffset,
tagName,
};
continue;
}
if (current.tagName !== 'example') {
current.lines.push(line);
}
}
flushCurrent();
return units;
}
function maskNeutralDescriptionFragments(value) {
return value
.replaceAll(/```[\s\S]*?```/gu, ' ')
.replaceAll(/`[^`\r\n]*`/gu, ' ')
.replaceAll(/\{@(?:code|link|linkcode|linkplain)\s[^}]+\}/giu, ' ')
.replaceAll(/\[([^\]]*)\]\((?:[^()]|\([^()]*\))*\)/gu, '$1')
.replaceAll(/\bhttps?:\/\/[^\s)]+/giu, ' ')
.replaceAll(/\{[A-Za-z_$][^{}\r\n]*\}/gu, ' ');
}
function getEnglishWords(value) {
return value.match(/[A-Za-z][\w$-]*/gu) ?? [];
}
function isStrongTechnicalWord(word) {
return (
/^[A-Z]{2}[A-Z0-9]*$/u.test(word) ||
/[0-9_$]/u.test(word) ||
/[a-z][A-Z]/u.test(word) ||
/[A-Z].*[A-Z]/u.test(word)
);
}
function normalizeDescriptionVerb(word) {
const normalizedWord = word.toLowerCase();
const withoutIng = normalizedWord.replace(/ing$/u, '');
const withoutEd = normalizedWord.replace(/ed$/u, '');
const candidates = [
normalizedWord,
normalizedWord.replace(/ies$/u, 'y'),
withoutIng,
`${withoutIng}e`,
withoutEd,
`${withoutEd}e`,
normalizedWord.replace(/es$/u, ''),
normalizedWord.replace(/s$/u, ''),
];
return candidates.find((candidate) =>
ENGLISH_DESCRIPTION_VERBS.has(candidate),
);
}
function hasEnglishClause(value) {
const ordinaryWords = getEnglishWords(value).filter(
(word) => !isStrongTechnicalWord(word),
);
const normalizedWords = ordinaryWords.map((word) => word.toLowerCase());
if (
ordinaryWords.length >= 2 &&
normalizedWords.some((word) => ENGLISH_FUNCTION_WORDS.has(word))
) {
return true;
}
if (
ordinaryWords.length >= 2 &&
normalizeDescriptionVerb(ordinaryWords[0]) !== undefined
) {
const verbIndex = value.indexOf(ordinaryWords[0]);
const prefix = verbIndex === -1 ? '' : value.slice(0, verbIndex);
const prefixHanCount = prefix.match(/\p{Script=Han}/gu)?.length ?? 0;
if (
prefix.trim() === '' ||
prefixHanCount <= 2 ||
/[,:]\s*$/u.test(prefix)
) {
return true;
}
}
const hanCount = value.match(/\p{Script=Han}/gu)?.length ?? 0;
return ordinaryWords.length >= 4 && ordinaryWords.length > hanCount;
}
function isPureTechnicalContinuation(value) {
const words = getEnglishWords(value);
if (words.length === 0 || words.length > 3) {
return false;
}
if (
words.some((word) => ENGLISH_FUNCTION_WORDS.has(word.toLowerCase())) ||
normalizeDescriptionVerb(words[0]) !== undefined
) {
return false;
}
return words.some(
(word) => isStrongTechnicalWord(word) || /^[A-Z][a-z]+$/u.test(word),
);
}
function findNonChineseDescription(rawComment) {
for (const unit of getJsdocDescriptionUnits(rawComment)) {
const maskedText = maskNeutralDescriptionFragments(unit.text);
const unitHasHan = HAN_CHARACTER_PATTERN.test(maskedText);
const lines = maskedText.split(/\r\n|\r|\n/gu);
for (const [lineOffset, line] of lines.entries()) {
const segments = line.split(/[。!?!?;]+|(?<=\.)\s+/gu);
for (const segment of segments) {
const normalizedSegment = segment.trim();
if (!/[\p{L}\p{N}]/u.test(normalizedSegment)) {
continue;
}
const segmentHasHan = HAN_CHARACTER_PATTERN.test(normalizedSegment);
if (!segmentHasHan) {
if (unitHasHan && isPureTechnicalContinuation(normalizedSegment)) {
continue;
}
return {
context: unit.context,
lineOffset: unit.lineOffset + lineOffset,
};
}
if (hasEnglishClause(normalizedSegment)) {
return {
context: unit.context,
lineOffset: unit.lineOffset + lineOffset,
};
}
}
}
}
return null;
}
function getScriptKind(filePath, language) { function getScriptKind(filePath, language) {
const normalizedLanguage = language?.toLowerCase(); const normalizedLanguage = language?.toLowerCase();
@ -166,19 +665,34 @@ 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 isSetupFunctionExpression(node, sourceFile) { function getFunctionExpressionRole(node, sourceFile) {
if (getNodeName(node, sourceFile) === 'setup') { let expression = node;
return true; 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 = node.parent; const parent = expression.parent;
return ( if (ts.isPropertyAssignment(parent) && parent.initializer === expression) {
ts.isPropertyAssignment(parent) && return getNodeName(parent, sourceFile);
getNodeName(parent, sourceFile) === 'setup' }
);
return getNodeName(node, sourceFile);
} }
function classifyTarget(node, sourceFile) { function classifyTarget(node, sourceFile) {
@ -202,9 +716,13 @@ function classifyTarget(node, sourceFile) {
if (!name) { if (!name) {
return { allowed: false, target: 'anonymous-function-expression' }; return { allowed: false, target: 'anonymous-function-expression' };
} }
if (isSetupFunctionExpression(node, sourceFile)) { const role = getFunctionExpressionRole(node, sourceFile);
if (role === 'setup') {
return { allowed: false, target: 'setup' }; return { allowed: false, target: 'setup' };
} }
if (role && LIFECYCLE_METHOD_NAMES.has(role)) {
return { allowed: false, target: 'lifecycle-entry' };
}
return { allowed: true, target: 'named-function-expression' }; return { allowed: true, target: 'named-function-expression' };
} }
@ -385,12 +903,19 @@ function inspectScriptBlock(filePath, block, fullSource, lineStarts) {
let rule = null; let rule = null;
let message = null; let message = null;
if (!classification.allowed) { if (classification.allowed) {
const invalidDescription = findNonChineseDescription(rawComment.text);
if (invalidDescription !== null) {
rule = 'non-chinese-description';
message = `JSDoc ${invalidDescription.context}的自然语言描述必须使用中文`;
location.line += invalidDescription.lineOffset;
if (invalidDescription.lineOffset > 0) {
location.column = 1;
}
}
} else {
rule = 'invalid-target'; rule = 'invalid-target';
message = `JSDoc 不允许用于 ${classification.target}`; message = `JSDoc 不允许用于 ${classification.target}`;
} else if (!HAN_CHARACTER_PATTERN.test(rawComment.text)) {
rule = 'non-chinese-description';
message = 'JSDoc 自然语言描述必须使用中文';
} }
return { return {
@ -457,12 +982,27 @@ function collectSyntaxTokens(source, scriptKind, filePath) {
} }
const text = node.getText(sourceFile); const text = node.getText(sourceFile);
if (text.length > 0) { if (text.length > 0) {
tokens.push([node.kind, text]); tokens.push({
end: node.end,
kind: node.kind,
start: node.getStart(sourceFile),
text,
});
} }
} }
visit(sourceFile); visit(sourceFile);
return tokens; const sortedTokens = tokens.toSorted(
(left, right) => left.start - right.start || left.end - right.end,
);
return sortedTokens.map((token, index) => {
const previousToken = sortedTokens[index - 1];
const hasLineTerminatorBefore =
previousToken === undefined
? false
: /[\r\n]/u.test(source.slice(previousToken.end, token.start));
return [token.kind, token.text, hasLineTerminatorBefore];
});
} }
export function getCodeTokenSignature(filePath, source) { export function getCodeTokenSignature(filePath, source) {
@ -591,6 +1131,21 @@ export function getTrackedCodeFiles(rootDirectory = process.cwd()) {
.toSorted(); .toSorted();
} }
export function getStagedCodeFiles(rootDirectory = process.cwd()) {
const output = execFileSync('git', ['ls-files', '--cached', '-z'], {
cwd: rootDirectory,
encoding: 'utf8',
env: getStagedGitEnvironment(rootDirectory),
});
return output
.split('\0')
.filter(Boolean)
.map((filePath) => filePath.replaceAll('\\', '/'))
.filter((filePath) => CODE_FILE_PATTERN.test(filePath))
.toSorted();
}
function summarizeInspections(inspections) { function summarizeInspections(inspections) {
const summary = { const summary = {
'invalid-target': 0, 'invalid-target': 0,
@ -631,6 +1186,24 @@ function inspectTrackedFiles(rootDirectory) {
}); });
} }
function inspectStagedFiles(rootDirectory) {
const environment = getStagedGitEnvironment(rootDirectory);
return getStagedCodeFiles(rootDirectory).map((filePath) => {
const source = execFileSync('git', ['show', `:${filePath}`], {
cwd: rootDirectory,
encoding: 'utf8',
env: environment,
maxBuffer: 64 * 1024 * 1024,
});
return {
absolutePath: null,
filePath,
result: inspectFileSource(filePath, source),
source,
};
});
}
function formatViolation(violation) { function formatViolation(violation) {
const blockSuffix = violation.block ? ` ${violation.block}` : ''; const blockSuffix = violation.block ? ` ${violation.block}` : '';
return `${violation.filePath}:${violation.line}:${violation.column} [${violation.rule}]${blockSuffix} ${violation.message}`; return `${violation.filePath}:${violation.line}:${violation.column} [${violation.rule}]${blockSuffix} ${violation.message}`;
@ -650,14 +1223,22 @@ function printSummary(summary, prefix = 'JSDoc 检查') {
function runCli() { function runCli() {
const args = new Set(process.argv.slice(2)); const args = new Set(process.argv.slice(2));
const fix = args.delete('--fix'); const fix = args.delete('--fix');
const staged = args.delete('--staged');
if (args.size > 0) { if (args.size > 0) {
console.error(`未知参数:${[...args].join(', ')}`); console.error(`未知参数:${[...args].join(', ')}`);
process.exitCode = 2; process.exitCode = 2;
return; return;
} }
if (fix && staged) {
console.error('暂存区检查不支持 --fix');
process.exitCode = 2;
return;
}
const rootDirectory = process.cwd(); const rootDirectory = process.cwd();
const inspections = inspectTrackedFiles(rootDirectory); const inspections = staged
? inspectStagedFiles(rootDirectory)
: inspectTrackedFiles(rootDirectory);
const initialSummary = summarizeInspections(inspections); const initialSummary = summarizeInspections(inspections);
if (!fix) { if (!fix) {
@ -666,7 +1247,7 @@ function runCli() {
console.error(formatViolation(violation)); console.error(formatViolation(violation));
} }
} }
printSummary(initialSummary); printSummary(initialSummary, staged ? 'JSDoc 暂存区检查' : 'JSDoc 检查');
if (initialSummary.violationCount > 0) { if (initialSummary.violationCount > 0) {
process.exitCode = 1; process.exitCode = 1;
} }

View File

@ -1,8 +1,16 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process'; import { execFileSync, spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import {
copyFileSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { runInNewContext } from 'node:vm';
import { it } from 'vitest'; import { it } from 'vitest';
@ -13,11 +21,24 @@ import {
inspectFileSource, inspectFileSource,
} from './check-jsdoc.mjs'; } from './check-jsdoc.mjs';
function runFixtureGit(repository, args) { const CHECKER_PATH = fileURLToPath(new URL('check-jsdoc.mjs', import.meta.url));
function runFixtureGit(repository, args, extraEnvironment = {}) {
const environment = Object.fromEntries( const environment = Object.fromEntries(
Object.entries(process.env).filter(([name]) => !name.startsWith('GIT_')), Object.entries(process.env).filter(([name]) => !name.startsWith('GIT_')),
); );
execFileSync('git', args, { cwd: repository, env: environment }); execFileSync('git', args, {
cwd: repository,
env: { ...environment, ...extraEnvironment },
});
}
function runStagedChecker(repository, extraEnvironment = {}) {
return spawnSync(process.execPath, [CHECKER_PATH, '--staged'], {
cwd: repository,
encoding: 'utf8',
env: { ...process.env, ...extraEnvironment },
});
} }
it('allows Chinese JSDoc only on explicitly named callable declarations', () => { it('allows Chinese JSDoc only on explicitly named callable declarations', () => {
@ -55,6 +76,228 @@ it('rejects pure-English JSDoc on an otherwise valid named function', () => {
assert.equal(result.violations[0]?.target, 'named-function-declaration'); assert.equal(result.violations[0]?.target, 'named-function-declaration');
}); });
it('rejects English prose hidden by Chinese text while allowing technical terms', () => {
const source = `
/** 通过 HTTP 连接 MySQL并按 userId 返回 DTO。 */
function loadValidData() {}
/** 用于获取loading的html模板。 */
function getLoadingTemplate() {}
/**
* 按用户标识查询
* @param {string} userId - 用户 ID
* @returns {Promise<Result>} 查询结果
* @example
* const 中文变量 = await loadValidData();
*/
function loadTaggedData() {}
/**
* 读取二进制内容
* @returns RequestResponse<Blob>
*/
function readBlobResponse() {}
/** 中文引子。 Determine whether there is permission from the user role. */
function checkAccess() {}
/**
* 格式化计数
* @param {string} value - The counter value.
* @param fields record
* @returns Counter text such as \`7890\` or \`12.3万\`.
*/
function formatCounter() {}
/**
* Parse the payload.
* @example
* const 中文变量 = parsePayload();
*/
function parsePayload() {}
/**
* 返回载荷
* @returns payload
*/
function readPayload() {}
`;
const result = inspectFileSource('mixed-language.ts', source);
assert.equal(result.comments.length, 8);
assert.deepEqual(
result.violations.map(({ rule, target }) => [rule, target]),
[
['non-chinese-description', 'named-function-declaration'],
['non-chinese-description', 'named-function-declaration'],
['non-chinese-description', 'named-function-declaration'],
['non-chinese-description', 'named-function-declaration'],
],
);
assert.deepEqual(
result.violations.map(({ message }) => message),
[
'JSDoc 主说明的自然语言描述必须使用中文',
'JSDoc @param的自然语言描述必须使用中文',
'JSDoc 主说明的自然语言描述必须使用中文',
'JSDoc @returns的自然语言描述必须使用中文',
],
);
assert.equal(
result.violations[1]?.line,
source.slice(0, source.indexOf('@param {string} value')).split('\n').length,
);
});
it('treats inline code as neutral and validates custom tag descriptions', () => {
const validSource = `
/**
* 使用 {@code access token} 完成认证
* @custom 中文扩展说明
* @public
*/
function authenticate() {}
`;
const invalidSource = `
/**
* 执行扩展检查
* @custom Determine whether the extension is enabled.
*/
function checkExtension() {}
`;
const codeMaskedSource = `
/**
* 执行载荷检查
* @custom payload {@code 中文变量}
*/
function checkPayload() {}
`;
assert.deepEqual(
inspectFileSource('inline-code.ts', validSource).violations,
[],
);
const invalidResult = inspectFileSource('custom-tag.ts', invalidSource);
assert.equal(invalidResult.violations.length, 1);
assert.equal(
invalidResult.violations[0]?.message,
'JSDoc @custom的自然语言描述必须使用中文',
);
assert.equal(
inspectFileSource('custom-code.ts', codeMaskedSource).violations.length,
1,
);
});
it('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(
inspectFileSource('structural-valid.ts', validSource).violations,
[],
);
const invalidResult = inspectFileSource(
'structural-invalid.ts',
invalidSource,
);
assert.equal(invalidResult.violations.length, 1);
assert.equal(
invalidResult.violations[0]?.message,
'JSDoc @typedef的自然语言描述必须使用中文',
);
});
it('does not parse fenced code or example decorators as JSDoc tags', () => {
const source = `
/**
* 展示装饰器示例
* \`\`\`ts
* @returns payload
* \`\`\`
* @example
* @Component
* class Demo {}
*/
function showDecoratorExample() {}
`;
assert.deepEqual(inspectFileSource('example.ts', source).violations, []);
});
it('does not share a Chinese anchor across main-description paragraphs', () => {
const invalidSource = `
/**
* 中文说明
*
* Cache expires tomorrow.
*/
function readCache() {}
`;
const validSource = `
/**
* 解析载荷
* HTTP payload
* 并返回结果
*/
function readPayload() {}
`;
assert.equal(
inspectFileSource('paragraph-invalid.ts', invalidSource).violations.length,
1,
);
assert.deepEqual(
inspectFileSource('paragraph-valid.ts', validSource).violations,
[],
);
});
it('rejects setup and lifecycle property function expressions by property role', () => {
const source = `
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 result = inspectFileSource('lifecycle-expression.ts', source);
assert.deepEqual(
result.violations.map(({ rule, target }) => [rule, target]),
[
['invalid-target', 'lifecycle-entry'],
['invalid-target', 'setup'],
['invalid-target', 'lifecycle-entry'],
['invalid-target', 'setup'],
['invalid-target', 'lifecycle-entry'],
],
);
});
it('rejects declarations and callables excluded by the policy', () => { it('rejects declarations and callables excluded by the policy', () => {
const source = ` const source = `
/** 接口说明。 */ /** 接口说明。 */
@ -223,6 +466,32 @@ const named = /** 保留具名函数表达式说明。 */ function namedExpressi
assert.equal(getCodeTokenSignature('cleanup.ts', fixed.source), beforeTokens); assert.equal(getCodeTokenSignature('cleanup.ts', fixed.source), beforeTokens);
}); });
it('preserves and compares line terminators around restricted productions', () => {
const source = `function readValue() {
return /** Remove this English description.
* It spans multiple lines.
*/ 1;
}`;
const unsafeSource = source.replace(
'/** Remove this English description.\n * It spans multiple lines.\n */ ',
'',
);
assert.notEqual(
getCodeTokenSignature('restricted.ts', unsafeSource),
getCodeTokenSignature('restricted.ts', source),
);
const fixed = applyJsdocPolicyFix('restricted.ts', source);
assert.equal(
getCodeTokenSignature('restricted.ts', fixed.source),
getCodeTokenSignature('restricted.ts', source),
);
assert.equal(runInNewContext(`${source}; readValue();`), undefined);
assert.equal(runInNewContext(`${fixed.source}; readValue();`), undefined);
});
it('tracked-file discovery also includes untracked non-ignored code', () => { it('tracked-file discovery also includes untracked non-ignored code', () => {
const repository = mkdtempSync(path.join(tmpdir(), 'admin-jsdoc-policy-')); const repository = mkdtempSync(path.join(tmpdir(), 'admin-jsdoc-policy-'));
@ -250,3 +519,67 @@ it('tracked-file discovery also includes untracked non-ignored code', () => {
rmSync(repository, { force: true, recursive: true }); rmSync(repository, { force: true, recursive: true });
} }
}); });
it('staged mode checks the actual index including a valid temporary index', () => {
const repository = mkdtempSync(path.join(tmpdir(), 'admin-jsdoc-staged-'));
const validSource = '/** 读取配置。 */\nfunction readConfig() {}\n';
const invalidSource =
'/** Read the current configuration. */\nfunction readConfig() {}\n';
try {
runFixtureGit(repository, ['init', '--quiet']);
runFixtureGit(repository, ['config', 'user.email', 'fixture@example.com']);
runFixtureGit(repository, ['config', 'user.name', 'Fixture']);
writeFileSync(path.join(repository, 'tracked.ts'), validSource);
writeFileSync(path.join(repository, 'deleted.ts'), validSource);
runFixtureGit(repository, ['add', 'tracked.ts', 'deleted.ts']);
runFixtureGit(repository, ['commit', '--quiet', '-m', 'baseline']);
writeFileSync(path.join(repository, 'tracked.ts'), invalidSource);
runFixtureGit(repository, ['add', 'tracked.ts']);
writeFileSync(path.join(repository, 'tracked.ts'), validSource);
assert.equal(runStagedChecker(repository).status, 1);
runFixtureGit(repository, ['add', 'tracked.ts']);
writeFileSync(path.join(repository, 'tracked.ts'), invalidSource);
assert.equal(runStagedChecker(repository).status, 0);
writeFileSync(path.join(repository, 'added.ts'), invalidSource);
runFixtureGit(repository, ['add', 'added.ts']);
assert.equal(runStagedChecker(repository).status, 1);
runFixtureGit(repository, ['rm', '--cached', '--quiet', 'added.ts']);
assert.equal(runStagedChecker(repository).status, 0);
rmSync(path.join(repository, 'deleted.ts'));
runFixtureGit(repository, ['add', '--update', 'deleted.ts']);
assert.equal(runStagedChecker(repository).status, 0);
writeFileSync(path.join(repository, 'tracked.ts'), validSource);
runFixtureGit(repository, ['add', 'tracked.ts']);
const indexPath = path.join(repository, '.git', 'index');
const temporaryIndexPath = path.join(
repository,
'.git',
'temporary-commit-index',
);
copyFileSync(indexPath, temporaryIndexPath);
writeFileSync(path.join(repository, 'tracked.ts'), invalidSource);
runFixtureGit(repository, ['add', 'tracked.ts'], {
GIT_INDEX_FILE: temporaryIndexPath,
});
writeFileSync(path.join(repository, 'tracked.ts'), validSource);
const indexBefore = readFileSync(indexPath);
const result = runStagedChecker(repository, {
GIT_COMMON_DIR: path.join(repository, 'hostile-common-dir'),
GIT_DIR: path.join(repository, 'hostile-git-dir'),
GIT_INDEX_FILE: temporaryIndexPath,
GIT_PREFIX: 'hostile-prefix/',
});
assert.equal(result.status, 1);
assert.match(result.stderr, /tracked\.ts/u);
assert.deepEqual(readFileSync(indexPath), indexBefore);
} finally {
rmSync(repository, { force: true, recursive: true });
}
}, 15_000);

View File

@ -10,6 +10,7 @@
"build:antdv-next": "pnpm run build --filter=@vben/web-antdv-next", "build:antdv-next": "pnpm run build --filter=@vben/web-antdv-next",
"check": "pnpm run check:type", "check": "pnpm run check:type",
"check:jsdoc": "pnpm run test:jsdoc && node internal/jsdoc-policy/check-jsdoc.mjs", "check:jsdoc": "pnpm run test:jsdoc && node internal/jsdoc-policy/check-jsdoc.mjs",
"check:jsdoc:staged": "pnpm run test:jsdoc && node internal/jsdoc-policy/check-jsdoc.mjs --staged",
"check:type": "turbo run typecheck", "check:type": "turbo run typecheck",
"clean": "rimraf .turbo apps/*/dist apps/*/dist.zip packages/**/dist packages/**/dist.zip internal/**/dist internal/**/dist.zip", "clean": "rimraf .turbo apps/*/dist apps/*/dist.zip packages/**/dist packages/**/dist.zip internal/**/dist internal/**/dist.zip",
"dev": "pnpm run dev:antdv-next", "dev": "pnpm run dev:antdv-next",

View File

@ -310,12 +310,6 @@ export class FormApi {
} }
} }
/**
*
* @param fields record
* @param filterFields schema中定义的字段 true
* @param shouldValidate
*/
async setValues( async setValues(
fields: Record<string, any>, fields: Record<string, any>,
filterFields: boolean = true, filterFields: boolean = true,

View File

@ -10,22 +10,12 @@ function useAccess() {
return preferences.app.accessMode; return preferences.app.accessMode;
}); });
/**
*
* @description: Determine whether there is permissionThe role is judged by the user's role
* @param roles
*/
function hasAccessByRoles(roles: string[]) { function hasAccessByRoles(roles: string[]) {
const userRoleSet = new Set(userStore.userRoles); const userRoleSet = new Set(userStore.userRoles);
const intersection = roles.filter((item) => userRoleSet.has(item)); const intersection = roles.filter((item) => userRoleSet.has(item));
return intersection.length > 0; return intersection.length > 0;
} }
/**
*
* @description: Determine whether there is permissionThe permission code is judged by the user's permission code
* @param codes
*/
function hasAccessByCodes(codes: string[]) { function hasAccessByCodes(codes: string[]) {
const userCodesSet = new Set(accessStore.accessCodes); const userCodesSet = new Set(accessStore.accessCodes);

View File

@ -51,10 +51,6 @@ const useTimezoneStore = defineStore(
() => { () => {
const timezoneRef = ref(getCurrentTimezone()); const timezoneRef = ref(getCurrentTimezone());
/**
*
* Initialize the timezone
*/
async function initTimezone() { async function initTimezone() {
const timezoneHandler = getTimezoneHandler(); const timezoneHandler = getTimezoneHandler();
const timezone = await timezoneHandler.getTimezone?.(); const timezone = await timezoneHandler.getTimezone?.();
@ -65,11 +61,6 @@ const useTimezoneStore = defineStore(
setCurrentTimezone(unref(timezoneRef)); setCurrentTimezone(unref(timezoneRef));
} }
/**
*
* Set the timezone
* @param timezone
*/
async function setTimezone(timezone: string) { async function setTimezone(timezone: string) {
const timezoneHandler = getTimezoneHandler(); const timezoneHandler = getTimezoneHandler();
await timezoneHandler.setTimezone?.(timezone); await timezoneHandler.setTimezone?.(timezone);
@ -78,10 +69,6 @@ const useTimezoneStore = defineStore(
setCurrentTimezone(timezone); setCurrentTimezone(timezone);
} }
/**
*
* Get the timezone options
*/
async function getTimezoneOptions() { async function getTimezoneOptions() {
const timezoneHandler = getTimezoneHandler(); const timezoneHandler = getTimezoneHandler();
return (await timezoneHandler.getTimezoneOptions?.()) || []; return (await timezoneHandler.getTimezoneOptions?.()) || [];