import assert from 'node:assert/strict';
import test from 'node:test';
import {
fixSourceText,
getTokenSignature,
validateSourceText,
} from './check-jsdoc-policy.mjs';
function issueCodes(sourceText, filePath = 'fixture.ts') {
return validateSourceText(filePath, sourceText).issues.map(
(issue) => issue.code,
);
}
test('accepts Chinese JSDoc on explicitly named functions and methods', () => {
const sourceText = `
/** 计算输入值。 */
function calculateValue() {}
const handler = /** 处理输入值。 */ function namedHandler() {};
class Example {
/** 读取当前值。 */
get value() {
return 1;
}
/** 写入当前值。 */
set value(nextValue) {}
/** 执行业务逻辑。 */
execute() {}
}
`;
assert.deepEqual(issueCodes(sourceText), []);
});
test('accepts necessary technical terms inside an overall Chinese description', () => {
const sourceText = `
/** 解析 HTTP payload 并返回 DTO。 */
function parsePayload() {}
`;
assert.deepEqual(issueCodes(sourceText), []);
});
test('rejects English-only or tag-only JSDoc on an otherwise valid target', () => {
const englishOnly = `
/** Parse the incoming payload. */
function parsePayload() {}
`;
const tagOnly = `
/** @returns payload */
function readPayload() {}
`;
assert.deepEqual(issueCodes(englishOnly), ['missing-chinese-description']);
assert.deepEqual(issueCodes(tagOnly), ['missing-chinese-description']);
});
test('rejects setup and Nest lifecycle entry JSDoc', () => {
const sourceText = `
class Example {
/** 初始化依赖。 */
setup() {}
/** 处理模块初始化。 */
onModuleInit() {}
}
`;
assert.deepEqual(issueCodes(sourceText), [
'forbidden-entry',
'forbidden-entry',
]);
});
test('rejects JSDoc on forbidden declaration and expression targets', () => {
const fixtures = [
`
class Example {
/** 创建实例。 */
constructor() {}
}
`,
`
/** 描述接口。 */
interface Example {}
`,
`
/** 描述类型。 */
type Example = string;
`,
`
class Example {
/** 保存值。 */
value = 1;
}
`,
`
/** 保存值。 */
const value = 1;
`,
`
const handler = /** 处理值。 */ () => true;
`,
`
const handler = /** 处理值。 */ function () {};
`,
`
items.map(/** 处理值。 */ () => true);
`,
`
items.map(/** 处理值。 */ function () {});
`,
];
for (const fixture of fixtures) {
assert.deepEqual(issueCodes(fixture), ['invalid-target']);
}
});
test('rejects a variable-level JSDoc even when the initializer is named', () => {
const sourceText = `
/** 处理值。 */
const handler = function namedHandler() {};
`;
assert.deepEqual(issueCodes(sourceText), ['invalid-target']);
});
test('validates TypeScript inside Vue script blocks with AST ownership', () => {
const sourceText = `
`;
assert.deepEqual(issueCodes(sourceText, 'Fixture.vue'), ['invalid-target']);
});
test('rejects unassociated raw JSDoc without matching literal text', () => {
const sourceText = `
const stringValue = '/** 字符串内容。 */';
const templateValue = \`/** 模板内容。 */\`;
const values = [/** 未关联的数组元素注释。 */ 1];
function calculateValue() {
return stringValue + templateValue + values[0];
}
`;
const beforeTokens = getTokenSignature('fixture.ts', sourceText);
const validation = validateSourceText('fixture.ts', sourceText);
const result = fixSourceText('fixture.ts', sourceText);
assert.deepEqual(
validation.issues.map((issue) => ({
code: issue.code,
ownerKind: issue.ownerKind,
})),
[{ code: 'invalid-target', ownerKind: 'UnattachedJSDoc' }],
);
assert.equal(result.removedCount, 1);
assert.match(result.sourceText, /字符串内容/);
assert.match(result.sourceText, /模板内容/);
assert.doesNotMatch(result.sourceText, /未关联的数组元素注释/);
assert.deepEqual(
getTokenSignature('fixture.ts', result.sourceText),
beforeTokens,
);
});
test('fixes only violating JSDoc and preserves every code token', () => {
const sourceText = `
/** Parse the value. */
function parseValue() {
return 1;
}
/** 保存默认值。 */
const defaultValue = 1;
const callbackResult = run('case', /** 执行回调。 */
() => true,
);
/** 计算结果。 */
function calculateResult() {
return parseValue() + defaultValue + Number(callbackResult);
}
`;
const beforeTokens = getTokenSignature('fixture.ts', sourceText);
const result = fixSourceText('fixture.ts', sourceText);
assert.equal(result.removedCount, 3);
assert.match(result.sourceText, /计算结果/);
assert.doesNotMatch(result.sourceText, /Parse the value|保存默认值|执行回调/);
assert.doesNotMatch(result.sourceText, /[^\S\r\n]+\r?$/mu);
assert.deepEqual(
validateSourceText('fixture.ts', result.sourceText).issues,
[],
);
assert.deepEqual(
getTokenSignature('fixture.ts', result.sourceText),
beforeTokens,
);
});