@@ -187,8 +187,19 @@ export const validateJSDocCodeblocksRule = /** @type {const} */ ({
187187} ) ;
188188
189189function extractTypeFromQuickInfo ( quickInfo ) {
190+ const { displayParts} = quickInfo ;
191+
192+ // For interfaces and enums, return everything after the keyword.
193+ const keywordIndex = displayParts . findIndex (
194+ part => part . kind === 'keyword' && [ 'interface' , 'enum' ] . includes ( part . text ) ,
195+ ) ;
196+
197+ if ( keywordIndex !== - 1 ) {
198+ return displayParts . slice ( keywordIndex + 1 ) . map ( part => part . text ) . join ( '' ) . trim ( ) ;
199+ }
200+
190201 let depth = 0 ;
191- const separatorIndex = quickInfo . displayParts . findIndex ( part => {
202+ const separatorIndex = displayParts . findIndex ( part => {
192203 if ( part . kind === 'punctuation' ) {
193204 if ( [ '(' , '{' , '<' ] . includes ( part . text ) ) {
194205 depth ++ ;
@@ -204,33 +215,65 @@ function extractTypeFromQuickInfo(quickInfo) {
204215 return false ;
205216 } ) ;
206217
207- let partsToUse = quickInfo . displayParts ;
208- if ( separatorIndex !== - 1 ) {
209- partsToUse = quickInfo . displayParts . slice ( separatorIndex + 1 ) ;
210- }
218+ // If `separatorIndex` is `-1` (not found), return the entire thing.
219+ return displayParts . slice ( separatorIndex + 1 ) . map ( part => part . text ) . join ( '' ) . trim ( ) ;
220+ }
211221
212- return partsToUse
213- . map ( ( part , index ) => {
214- const { kind, text} = part ;
222+ function normalizeUnions ( type ) {
223+ const sourceFile = ts . createSourceFile (
224+ 'twoslash-type.ts' ,
225+ `declare const test: ${ type } ;` ,
226+ ts . ScriptTarget . Latest ,
227+ ) ;
228+
229+ const typeNode = sourceFile . statements [ 0 ] . declarationList . declarations [ 0 ] . type ;
230+
231+ const print = node => ts . createPrinter ( ) . printNode ( ts . EmitHint . Unspecified , node , sourceFile ) ;
232+ const isNumeric = v => v . trim ( ) !== '' && Number . isFinite ( Number ( v ) ) ;
233+
234+ const visit = node => {
235+ node = ts . visitEachChild ( node , visit , undefined ) ;
236+
237+ if ( ts . isUnionTypeNode ( node ) ) {
238+ const types = node . types
239+ . map ( t => [ print ( t ) , t ] )
240+ . sort ( ( [ a ] , [ b ] ) =>
241+ // Numbers are sorted only wrt other numbers.
242+ isNumeric ( a ) && isNumeric ( b ) ? Number ( a ) - Number ( b ) : 0 ,
243+ )
244+ . map ( t => t [ 1 ] ) ;
245+
246+ return ts . factory . updateUnionTypeNode (
247+ node ,
248+ ts . factory . createNodeArray ( types ) ,
249+ ) ;
250+ }
215251
216- // Replace spaces used for indentation with tabs
217- const previousPart = partsToUse [ index - 1 ] ;
218- if ( kind === 'space' && ( index === 0 || previousPart ?. kind === 'lineBreak' ) ) {
219- return text . replaceAll ( ' ' , '\t' ) ;
220- }
252+ // Prefer single-line formatting for tuple types.
253+ if ( ts . isTupleTypeNode ( node ) ) {
254+ const updated = ts . factory . createTupleTypeNode ( node . elements ) ;
255+ ts . setEmitFlags ( updated , ts . EmitFlags . SingleLine ) ;
256+ return updated ;
257+ }
221258
222- // Replace double-quoted string literals with single-quoted ones
223- if ( kind === 'stringLiteral' && text . startsWith ( '"' ) && text . endsWith ( '"' ) ) {
224- return `'${ text
225- . slice ( 1 , - 1 )
226- . replaceAll ( String . raw `\"` , '"' )
227- . replaceAll ( '\'' , String . raw `\'` ) } '`;
228- }
259+ // Replace double-quoted string literals with single-quoted ones.
260+ if ( ts . isStringLiteral ( node ) ) {
261+ const updated = ts . factory . createStringLiteral ( node . text , true ) ;
262+ // Preserve non-ASCII characters like emojis.
263+ ts . setEmitFlags ( updated , ts . EmitFlags . NoAsciiEscaping ) ;
264+ return updated ;
265+ }
266+
267+ return node ;
268+ } ;
229269
230- return text ;
231- } )
232- . join ( '' )
233- . trim ( ) ;
270+ return print ( visit ( typeNode ) ) . replaceAll ( / ^ ( + ) / gm, indentation => {
271+ // Replace spaces used for indentation with tabs
272+ const spacesPerTab = 4 ;
273+ const tabCount = Math . floor ( indentation . length / spacesPerTab ) ;
274+ const remainingSpaces = indentation . length % spacesPerTab ;
275+ return '\t' . repeat ( tabCount ) + ' ' . repeat ( remainingSpaces ) ;
276+ } ) ;
234277}
235278
236279function validateTwoslashTypes ( context , env , code , codeStartIndex ) {
@@ -268,7 +311,7 @@ function validateTwoslashTypes(context, env, code, codeStartIndex) {
268311 const quickInfo = env . languageService . getQuickInfoAtPosition ( FILENAME , previousLineOffset + i ) ;
269312
270313 if ( quickInfo ?. displayParts ) {
271- let expectedType = extractTypeFromQuickInfo ( quickInfo ) ;
314+ let expectedType = normalizeUnions ( extractTypeFromQuickInfo ( quickInfo ) ) ;
272315
273316 if ( expectedType . length < 80 ) {
274317 expectedType = expectedType
0 commit comments