@@ -186,6 +186,15 @@ export const validateJSDocCodeblocksRule = /** @type {const} */ ({
186186 } ,
187187} ) ;
188188
189+ function getLeftmostQuickInfo ( env , line , lineOffset ) {
190+ for ( let i = 0 ; i < line . length ; i ++ ) {
191+ const quickInfo = env . languageService . getQuickInfoAtPosition ( FILENAME , lineOffset + i ) ;
192+ if ( quickInfo ?. displayParts ) {
193+ return quickInfo ;
194+ }
195+ }
196+ }
197+
189198function extractTypeFromQuickInfo ( quickInfo ) {
190199 const { displayParts} = quickInfo ;
191200
@@ -219,7 +228,7 @@ function extractTypeFromQuickInfo(quickInfo) {
219228 return displayParts . slice ( separatorIndex + 1 ) . map ( part => part . text ) . join ( '' ) . trim ( ) ;
220229}
221230
222- function normalizeUnions ( type ) {
231+ function normalizeType ( type , onlySortNumbers = false ) {
223232 const sourceFile = ts . createSourceFile (
224233 'twoslash-type.ts' ,
225234 `declare const test: ${ type } ;` ,
@@ -229,6 +238,7 @@ function normalizeUnions(type) {
229238 const typeNode = sourceFile . statements [ 0 ] . declarationList . declarations [ 0 ] . type ;
230239
231240 const print = node => ts . createPrinter ( ) . printNode ( ts . EmitHint . Unspecified , node , sourceFile ) ;
241+
232242 const isNumeric = v => v . trim ( ) !== '' && Number . isFinite ( Number ( v ) ) ;
233243
234244 const visit = node => {
@@ -237,9 +247,12 @@ function normalizeUnions(type) {
237247 if ( ts . isUnionTypeNode ( node ) ) {
238248 const types = node . types
239249 . 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 ,
250+ . sort (
251+ ( [ a ] , [ b ] ) => isNumeric ( a ) && isNumeric ( b )
252+ ? Number ( a ) - Number ( b )
253+ : ( onlySortNumbers
254+ ? 0 // Numbers are sorted only wrt other numbers
255+ : a . localeCompare ( b ) ) ,
243256 )
244257 . map ( t => t [ 1 ] ) ;
245258
@@ -276,6 +289,20 @@ function normalizeUnions(type) {
276289 } ) ;
277290}
278291
292+ function getCommentForType ( type ) {
293+ let comment = type ;
294+
295+ if ( type . length < 80 ) {
296+ comment = type
297+ . replaceAll ( / \r ? \n \s * / g, ' ' ) // Collapse into single line
298+ . replaceAll ( / { \s + / g, '{' ) // Remove spaces after `{`
299+ . replaceAll ( / \s + } / g, '}' ) // Remove spaces before ` }`
300+ . replaceAll ( / ; (? = } ) / g, '' ) ; // Remove semicolons before ` }`
301+ }
302+
303+ return `${ TWOSLASH_COMMENT } ${ comment . replaceAll ( '\n' , '\n// ' ) } ` ;
304+ }
305+
279306function validateTwoslashTypes ( context , env , code , codeStartIndex ) {
280307 const sourceFile = env . languageService . getProgram ( ) . getSourceFile ( FILENAME ) ;
281308 const lines = code . split ( '\n' ) ;
@@ -307,31 +334,27 @@ function validateTwoslashTypes(context, env, code, codeStartIndex) {
307334 const previousLine = lines [ previousLineIndex ] ;
308335 const previousLineOffset = sourceFile . getPositionOfLineAndCharacter ( previousLineIndex , 0 ) ;
309336
310- for ( let i = 0 ; i < previousLine . length ; i ++ ) {
311- const quickInfo = env . languageService . getQuickInfoAtPosition ( FILENAME , previousLineOffset + i ) ;
337+ const actualCommentIndex = line . indexOf ( TWOSLASH_COMMENT ) ;
312338
313- if ( quickInfo ?. displayParts ) {
314- let expectedType = normalizeUnions ( extractTypeFromQuickInfo ( quickInfo ) ) ;
339+ const actualCommentStartOffset = sourceFile . getPositionOfLineAndCharacter ( index , actualCommentIndex ) ;
340+ const actualCommentEndOffset = sourceFile . getPositionOfLineAndCharacter ( actualCommentEndLine , lines [ actualCommentEndLine ] . length ) ;
315341
316- if ( expectedType . length < 80 ) {
317- expectedType = expectedType
318- . replaceAll ( / \r ? \n \s * / g, ' ' ) // Collapse into single line
319- . replaceAll ( / { \s + / g, '{' ) // Remove spaces after `{`
320- . replaceAll ( / \s + } / g, '}' ) // Remove spaces before ` }`
321- . replaceAll ( / ; (? = } ) / g, '' ) ; // Remove semicolons before ` }`
322- }
342+ const start = codeStartIndex + actualCommentStartOffset ;
343+ const end = codeStartIndex + actualCommentEndOffset ;
323344
324- const expectedComment = TWOSLASH_COMMENT + ' ' + expectedType . replaceAll ( '\n' , '\n// ' ) ;
345+ const quickInfo = getLeftmostQuickInfo ( env , previousLine , previousLineOffset ) ;
325346
326- if ( actualComment !== expectedComment ) {
327- const actualCommentIndex = line . indexOf ( TWOSLASH_COMMENT ) ;
347+ if ( quickInfo ?. displayParts ) {
348+ const rawActualType = actualComment . slice ( TWOSLASH_COMMENT . length ) . replaceAll ( '\n// ' , '\n' ) ;
328349
329- const actualCommentStartOffset = sourceFile . getPositionOfLineAndCharacter ( index , actualCommentIndex ) ;
330- const actualCommentEndOffset = sourceFile . getPositionOfLineAndCharacter ( actualCommentEndLine , lines [ actualCommentEndLine ] . length ) ;
350+ const expectedType = normalizeType ( extractTypeFromQuickInfo ( quickInfo ) ) ;
351+ const actualType = normalizeType ( rawActualType ) ;
331352
332- const start = codeStartIndex + actualCommentStartOffset ;
333- const end = codeStartIndex + actualCommentEndOffset ;
353+ if ( actualType === expectedType ) {
354+ // If the types are equal, check for formatting errors and unordered numbers in unions
355+ const expectedComment = getCommentForType ( normalizeType ( rawActualType , true ) ) ;
334356
357+ if ( actualComment !== expectedComment ) {
335358 context . report ( {
336359 loc : {
337360 start : context . sourceCode . getLocFromIndex ( start ) ,
@@ -352,8 +375,28 @@ function validateTwoslashTypes(context, env, code, codeStartIndex) {
352375 } ,
353376 } ) ;
354377 }
355-
356- break ;
378+ } else {
379+ const expectedComment = getCommentForType ( expectedType ) ;
380+
381+ context . report ( {
382+ loc : {
383+ start : context . sourceCode . getLocFromIndex ( start ) ,
384+ end : context . sourceCode . getLocFromIndex ( end ) ,
385+ } ,
386+ messageId : 'typeMismatch' ,
387+ data : {
388+ expectedComment,
389+ actualComment,
390+ } ,
391+ fix ( fixer ) {
392+ const indent = line . slice ( 0 , actualCommentIndex ) ;
393+
394+ return fixer . replaceTextRange (
395+ [ start , end ] ,
396+ expectedComment . replaceAll ( '\n' , `\n${ indent } ` ) ,
397+ ) ;
398+ } ,
399+ } ) ;
357400 }
358401 }
359402 }
0 commit comments