import assert from "assert" ; import * as types from "ast-types" ; const n = types . namedTypes ; const isArray = types . builtInTypes . array ; const isObject = types . builtInTypes . object ; import { Lines , concat } from "./lines" ; import { comparePos , fixFaultyLocations } from "./util" ; type Node = types . namedTypes . Node ; type Comment = types . namedTypes . Comment ; const childNodesCache = new WeakMap < Node , Node [ ] > ( ) ; function getSortedChildNodes ( node , lines , resultArray ? ) { if ( ! node ) { return resultArray ; } fixFaultyLocations ( node , lines ) ; if ( resultArray ) { if ( n . Node . check ( node ) && n . SourceLocation . check ( node . loc ) ) { let i = resultArray . length - 1 ; for ( ; i >= 0 ; -- i ) { const child = resultArray [ i ] ; if ( child && child . loc && comparePos ( child . loc . end , node . loc . start ) <= 0 ) { break ; } } resultArray . splice ( i + 1 , 0 , node ) ; return resultArray ; } } else { const childNodes = childNodesCache . get ( node ) ; if ( childNodes ) { return childNodes ; } } let names ; if ( isArray . check ( node ) ) { names = Object . keys ( node ) ; } else if ( isObject . check ( node ) ) { names = types . getFieldNames ( node ) ; } else { return resultArray ; } if ( ! resultArray ) { childNodesCache . set ( node , ( resultArray = [ ] ) ) ; } for ( let i = 0 , nameCount = names . length ; i < nameCount ; ++ i ) { getSortedChildNodes ( ( node as any ) [ names [ i ] ] , lines , resultArray ) ; } return resultArray ; } function decorateComment ( node , comment , lines ) { const childNodes = getSortedChildNodes ( node , lines ) ; let left = 0 ; let right = childNodes && childNodes . length ; let precedingNode ; let followingNode ; while ( typeof right === "number" && left < right ) { const middle = ( left + right ) >> 1 ; const child = childNodes ! [ middle ] ; if ( comparePos ( child . loc ! . start , comment . loc ! . start ) <= 0 && comparePos ( comment . loc ! . end , child . loc ! . end ) <= 0 ) { decorateComment ( ( ( comment as any ) . enclosingNode = child ) , comment , lines ) ; return ; } if ( comparePos ( child . loc ! . end , comment . loc ! . start ) <= 0 ) { precedingNode = child ; left = middle + 1 ; continue ; } if ( comparePos ( comment . loc ! . end , child . loc ! . start ) <= 0 ) { followingNode = child ; right = middle ; continue ; } throw new Error ( "Comment location overlaps with node location" ) ; } if ( precedingNode ) { ( comment as any ) . precedingNode = precedingNode ; } if ( followingNode ) { ( comment as any ) . followingNode = followingNode ; } } export function attach ( comments , ast , lines ) { if ( ! isArray . check ( comments ) ) { return ; } const tiesToBreak = [ ] ; comments . forEach ( function ( comment ) { comment . loc . lines = lines ; decorateComment ( ast , comment , lines ) ; const pn = comment . precedingNode ; const en = comment . enclosingNode ; const fn = comment . followingNode ; if ( pn && fn ) { const tieCount = tiesToBreak . length ; if ( tieCount > 0 ) { const lastTie = tiesToBreak [ tieCount - 1 ] ; assert . strictEqual ( lastTie . precedingNode === comment . precedingNode , lastTie . followingNode === comment . followingNode ) ; if ( lastTie . followingNode !== comment . followingNode ) { breakTies ( tiesToBreak , lines ) ; } } tiesToBreak . push ( comment ) ; } else if ( pn ) { breakTies ( tiesToBreak , lines ) ; addTrailingComment ( pn , comment ) ; } else if ( fn ) { breakTies ( tiesToBreak , lines ) ; addLeadingComment ( fn , comment ) ; } else if ( en ) { breakTies ( tiesToBreak , lines ) ; addDanglingComment ( en , comment ) ; } else { throw new Error ( "AST contains no nodes at all?" ) ; } } ) ; breakTies ( tiesToBreak , lines ) ; comments . forEach ( function ( comment ) { delete comment . precedingNode ; delete comment . enclosingNode ; delete comment . followingNode ; } ) ; } function breakTies ( tiesToBreak , lines ) { const tieCount = tiesToBreak . length ; if ( tieCount === 0 ) { return ; } const pn = tiesToBreak [ 0 ] . precedingNode ; const fn = tiesToBreak [ 0 ] . followingNode ; let gapEndPos = fn . loc . start ; let indexOfFirstLeadingComment = tieCount ; let comment ; for ( ; indexOfFirstLeadingComment > 0 ; -- indexOfFirstLeadingComment ) { comment = tiesToBreak [ indexOfFirstLeadingComment - 1 ] ; assert . strictEqual ( comment . precedingNode , pn ) ; assert . strictEqual ( comment . followingNode , fn ) ; const gap = lines . sliceString ( comment . loc . end , gapEndPos ) ; if ( /\S/ . test ( gap ) ) { break ; } gapEndPos = comment . loc . start ; } while ( indexOfFirstLeadingComment <= tieCount && ( comment = tiesToBreak [ indexOfFirstLeadingComment ] ) && ( comment . type === "Line" || comment . type === "CommentLine" ) && comment . loc . start . column > fn . loc . start . column ) { ++ indexOfFirstLeadingComment ; } tiesToBreak . forEach ( function ( comment , i ) { if ( i < indexOfFirstLeadingComment ) { addTrailingComment ( pn , comment ) ; } else { addLeadingComment ( fn , comment ) ; } } ) ; tiesToBreak . length = 0 ; } function addCommentHelper ( node , comment ) { const comments = node . comments || ( node . comments = [ ] ) ; comments . push ( comment ) ; } function addLeadingComment ( node , comment ) { comment . leading = true ; comment . trailing = false ; addCommentHelper ( node , comment ) ; } function addDanglingComment ( node , comment ) { comment . leading = false ; comment . trailing = false ; addCommentHelper ( node , comment ) ; } function addTrailingComment ( node , comment ) { comment . leading = false ; comment . trailing = true ; addCommentHelper ( node , comment ) ; } function printLeadingComment ( commentPath , print ) { const comment = commentPath . getValue ( ) ; n . Comment . assert ( comment ) ; const loc = comment . loc ; const lines = loc && loc . lines ; const parts = [ print ( commentPath ) ] ; if ( comment . trailing ) { parts . push ( "\n" ) ; } else if ( lines instanceof Lines ) { const trailingSpace = lines . slice ( loc . end , lines . skipSpaces ( loc . end ) || lines . lastPos ( ) ) ; if ( trailingSpace . length === 1 ) { parts . push ( trailingSpace ) ; } else { parts . push ( new Array ( trailingSpace . length ) . join ( "\n" ) ) ; } } else { parts . push ( "\n" ) ; } return concat ( parts ) ; } function printTrailingComment ( commentPath , print ) { const comment = commentPath . getValue ( commentPath ) ; n . Comment . assert ( comment ) ; const loc = comment . loc ; const lines = loc && loc . lines ; const parts = [ ] ; if ( lines instanceof Lines ) { const fromPos = lines . skipSpaces ( loc . start , true ) || lines . firstPos ( ) ; const leadingSpace = lines . slice ( fromPos , loc . start ) ; if ( leadingSpace . length === 1 ) { parts . push ( leadingSpace ) ; } else { parts . push ( new Array ( leadingSpace . length ) . join ( "\n" ) ) ; } } parts . push ( print ( commentPath ) ) ; return concat ( parts ) ; } export function printComments ( path , print ) { const value = path . getValue ( ) ; const innerLines = print ( path ) ; const comments = n . Node . check ( value ) && types . getFieldValue ( value , "comments" ) ; if ( ! comments || comments . length === 0 ) { return innerLines ; } const leadingParts = [ ] ; const trailingParts = [ innerLines ] ; path . each ( function ( commentPath ) { const comment = commentPath . getValue ( ) ; const leading = types . getFieldValue ( comment , "leading" ) ; const trailing = types . getFieldValue ( comment , "trailing" ) ; if ( leading || ( trailing && ! ( n . Statement . check ( value ) || comment . type === "Block" || comment . type === "CommentBlock" ) ) ) { leadingParts . push ( printLeadingComment ( commentPath , print ) ) ; } else if ( trailing ) { trailingParts . push ( printTrailingComment ( commentPath , print ) ) ; } } , "comments" ) ; leadingParts . push . apply ( leadingParts , trailingParts ) ; return concat ( leadingParts ) ; }