From eeb988981c165ce83343b7d500a8a94dac9d2cec Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Tue, 3 Feb 2026 23:42:05 +0900 Subject: [PATCH] fix: `validate-jsdoc-codeblock` accepts union symbols --- lint-rules/validate-jsdoc-codeblocks.js | 46 ++++++++++++++------ lint-rules/validate-jsdoc-codeblocks.test.js | 20 +++++++++ 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/lint-rules/validate-jsdoc-codeblocks.js b/lint-rules/validate-jsdoc-codeblocks.js index 46871e67f..acc063786 100644 --- a/lint-rules/validate-jsdoc-codeblocks.js +++ b/lint-rules/validate-jsdoc-codeblocks.js @@ -239,7 +239,7 @@ function normalizeUnions(type) { .map(t => [print(t), t]) .sort(([a], [b]) => // Numbers are sorted only wrt other numbers - isNumeric(a) && isNumeric(b) ? Number(a) - Number(b) : 0, + isNumeric(a) && isNumeric(b) ? Number(a) - Number(b) : a.localeCompare(b), ) .map(t => t[1]); @@ -276,6 +276,18 @@ function normalizeUnions(type) { }); } +function flattenObjectExpression(maybeObjectExpression) { + return maybeObjectExpression + .replaceAll(/\r?\n\s*/g, ' ') // Collapse into single line + .replaceAll(/{\s+/g, '{') // Remove spaces after `{` + .replaceAll(/\s+}/g, '}') // Remove spaces before `}` + .replaceAll(/;(?=})/g, ''); // Remove semicolons before `}` +} + +function toArrowCommentExpression(typeExpression) { + return TWOSLASH_COMMENT + ' ' + typeExpression.replaceAll('\n', '\n// '); +} + function validateTwoslashTypes(context, env, code, codeStartIndex) { const sourceFile = env.languageService.getProgram().getSourceFile(FILENAME); const lines = code.split('\n'); @@ -311,19 +323,25 @@ function validateTwoslashTypes(context, env, code, codeStartIndex) { const quickInfo = env.languageService.getQuickInfoAtPosition(FILENAME, previousLineOffset + i); if (quickInfo?.displayParts) { - let expectedType = normalizeUnions(extractTypeFromQuickInfo(quickInfo)); - - if (expectedType.length < 80) { - expectedType = expectedType - .replaceAll(/\r?\n\s*/g, ' ') // Collapse into single line - .replaceAll(/{\s+/g, '{') // Remove spaces after `{` - .replaceAll(/\s+}/g, '}') // Remove spaces before `}` - .replaceAll(/;(?=})/g, ''); // Remove semicolons before `}` + const expectedType = normalizeUnions(extractTypeFromQuickInfo(quickInfo)); + + // Save both `expectedComment` and `actualComment` to pass them to report. + let saveExpectedType = expectedType; + if (saveExpectedType.length < 80) { + saveExpectedType = flattenObjectExpression(saveExpectedType); } - const expectedComment = TWOSLASH_COMMENT + ' ' + expectedType.replaceAll('\n', '\n// '); + const saveExpectedComment = toArrowCommentExpression(saveExpectedType); + const saveActualComment = actualComment; + // To compare with `actualComment`. + const expectedComment = toArrowCommentExpression(flattenObjectExpression(expectedType)); + + const isPassed = (/\s*\/\/=>\s\S+/g).test(actualComment); + const isNotPassed = (/\/\/}/g.test(actualComment)) || (actualComment.length < 80 && (/\[{\n/g.test(actualComment))); + + actualComment = flattenObjectExpression(TWOSLASH_COMMENT + ' ' + normalizeUnions(actualComment.replaceAll(/(\/\/=>)|(\/\/)/g, ''))); - if (actualComment !== expectedComment) { + if (!isPassed || isNotPassed || actualComment !== expectedComment) { const actualCommentIndex = line.indexOf(TWOSLASH_COMMENT); const actualCommentStartOffset = sourceFile.getPositionOfLineAndCharacter(index, actualCommentIndex); @@ -339,15 +357,15 @@ function validateTwoslashTypes(context, env, code, codeStartIndex) { }, messageId: 'typeMismatch', data: { - expectedComment, - actualComment, + expectedComment: saveExpectedComment, + actualComment: saveActualComment, }, fix(fixer) { const indent = line.slice(0, actualCommentIndex); return fixer.replaceTextRange( [start, end], - expectedComment.replaceAll('\n', `\n${indent}`), + saveExpectedComment.replaceAll('\n', `\n${indent}`), ); }, }); diff --git a/lint-rules/validate-jsdoc-codeblocks.test.js b/lint-rules/validate-jsdoc-codeblocks.test.js index 6058040de..24391f053 100644 --- a/lint-rules/validate-jsdoc-codeblocks.test.js +++ b/lint-rules/validate-jsdoc-codeblocks.test.js @@ -805,6 +805,26 @@ ruleTester.run('validate-jsdoc-codeblocks', validateJSDocCodeblocksRule, { //=> '🦄' | '🐶' | '🐇' `))), + // Union symbols are sorted. + exportTypeAndOption(jsdoc(fence(dedenter` + namespace AAA { + export type aaa = {a: string}; + } + namespace BBB { + export type aaa = {a: string}; + } + namespace CCC { + export type aaa = {a: string}; + export type bbb = {a: string}; + } + + type Symbols_0 = AAA.aaa | CCC.bbb | CCC.aaa | BBB.aaa; + //=> AAA.aaa | BBB.aaa | CCC.aaa | CCC.bbb + + type Symbols_1 = AAA.aaa | CCC.bbb | CCC.aaa | BBB.aaa; + //=> BBB.aaa | AAA.aaa | CCC.aaa | CCC.bbb + `))), + // === Different types of quick info === // Function exportTypeAndOption(jsdoc(fence(dedenter`