import * as chalk from 'chalk' ; import * as ts from 'typescript' ; import { Result , Ok as MonadsOk , Err as MonadsErr } from '@ts-std/monads' ; import { NonEmpty } from './utils' ; export type SourceFile = Readonly < { source : string ; filename ? : string ; } > ; export type Span = Readonly < { file : SourceFile ; start : number ; end : number ; line : number ; column : number ; } > ; export function Span ( file , start , text , line , column ) { return { file , start , end : start + text . length , line , column } ; } export namespace Span { export function fromTsNode ( sourceFile , { pos : start , end } ) { const { line : zeroLine , character : column } = sourceFile . getLineAndCharacterOfPosition ( start ) ; const { text : source , fileName : filename } = sourceFile ; return { file : { source , filename } , start , end , line : zeroLine + 1 , column } ; } } export type SpanError = Readonly < { region : Span | string ; title : string ; message : string ; error : true ; } > ; export function SpanError ( region , title , paragraphs ) { return { region , title , message : paragraphs . join ( '\n' ) , error : true } as SpanError ; } export type SpanWarning = Readonly < { region : Span | string ; title : string ; message : string ; error : false ; } > ; export function SpanWarning ( region , title , paragraphs ) { return { region , title , message : paragraphs . join ( '\n' ) , error : false } as SpanWarning ; } type W = { warnings ? : SpanWarning [ ] ; } ; export type SpanResult < T > = Result < { value : T ; } & W , { errors : NonEmpty < SpanError > ; } & W > ; export namespace SpanResult { export function Ok < T > ( value , warnings ? ) { return MonadsOk ( { value , warnings } ) ; } export function TsNodeErr ( sourceFile , node , title , paragraphs ) { return MonadsErr ( { errors : [ SpanError ( Span . fromTsNode ( sourceFile , node ) , title , paragraphs ) ] } ) ; } export function Err ( fileName , title , paragraphs ) { return MonadsErr ( { errors : [ SpanError ( fileName , title , paragraphs ) ] } ) ; } export type UnSpan < R extends SpanResult < any > > = R extends SpanResult < infer T > ? Result < T , void > : never ; export function checkSuccess ( errors , warnings ) { return [ errors . length ? MonadsErr ( errors as NonEmpty < SpanError > ) : MonadsOk ( undefined as void ) , warnings ] ; } export class Context { protected errors : SpanError [ ] = [ ] ; protected warnings : SpanWarning [ ] = [ ] ; drop ( ) { const errors = this . errors . splice ( 0 , this . errors . length ) ; const warnings = this . warnings . splice ( 0 , this . warnings . length ) ; return { errors , warnings } ; } constructor ( readonly sourceFile ) { } subsume < T > ( result ) { if ( result . is_ok ( ) ) { const { value , warnings } = result . value ; if ( warnings && warnings . length ) this . warnings = this . warnings . concat ( warnings ) ; return MonadsOk ( value ) ; } const { errors , warnings } = result . error ; if ( warnings && warnings . length ) this . warnings = this . warnings . concat ( warnings ) ; this . errors = this . errors . concat ( errors ) ; return MonadsErr ( undefined as void ) ; } tsNodeWarn ( node , title , paragraphs ) { this . warnings . push ( SpanWarning ( Span . fromTsNode ( this . sourceFile , node ) , title , paragraphs ) ) ; } warn ( fileName , title , paragraphs ) { this . warnings . push ( SpanWarning ( fileName , title , paragraphs ) ) ; } Err ( node , title , ... paragraphs ) { this . errors . push ( SpanError ( Span . fromTsNode ( this . sourceFile , node ) , title , paragraphs ) ) ; return MonadsErr ( undefined as void ) ; } } } export function formatDiagnostics ( errors , warnings , lineWidth ) { return ( errors as ( SpanError | SpanWarning ) [ ] ) . concat ( warnings ) . map ( d => formatDiagnostic ( d , lineWidth ) ) . join ( '\n\n\n' ) + '\n' ; } const info = chalk . blue . bold ; const file = chalk . magentaBright . bold ; function clean ( s ) { return s . replace ( /\t/g , '  ' ) ; } export function formatDiagnostic ( { region , title , message , error } , lineWidth ) { const header = info ( `-- ${ title } ` + '-' . repeat ( lineWidth - ( title . length + 4 ) ) ) ; if ( typeof region === 'string' ) { const fileHeader = '\n' + ' ' . repeat ( 3 ) + file ( region ) ; return header + '\n' + fileHeader + formatMessage ( message , '\n  ' , lineWidth ) ; } const { file : { source , filename } , start , end , line , column } = region ; const highlight = error ? chalk . red . bold : chalk . yellow . bold ; const lineNumberWidth = line . toString ( ) . length ; function makeGutter ( lineNumber ? , error = false ) { const insert = lineNumber !== undefined ? ' ' . repeat ( lineNumberWidth - lineNumber . toString ( ) . length ) + info ( lineNumber ) : ' ' . repeat ( lineNumberWidth ) ; return `\n ${ insert } ${ info ( '|' ) }${ error ? highlight ( '>' ) : ' ' } ` ; } const blankGutter = makeGutter ( ) ; const margin = `\n${ ' ' . repeat ( lineNumberWidth ) }  ` ; const messageLine = '\n' + formatMessage ( message , margin , lineWidth ) ; const fileHeader = ( filename ? '\n' + ' ' . repeat ( lineNumberWidth + 3 ) + file ( filename ) : '' ) ; let sourceLineStart = start ; for ( ; sourceLineStart >= 0 ; sourceLineStart -- ) if ( source [ sourceLineStart ] === '\n' ) break ; const lines = [ ] ; let currentLineNumber = line ; let currentLineStart = sourceLineStart ; while ( currentLineStart < end ) { let currentLineEnd = source . indexOf ( '\n' , currentLineStart + 1 ) ; if ( currentLineEnd < 0 ) break ; lines . push ( [ currentLineNumber , source . slice ( currentLineStart + 1 , currentLineEnd ) ] ) ; currentLineStart = currentLineEnd ; currentLineNumber ++ ; } switch ( lines . length ) { case 0 : throw new Error ( 'zuh?' ) ; case 1 : const [ [ , sourceLine ] ] = lines ; const printSourceLine = clean ( sourceLine ) ; const pointerPrefix = ' ' . repeat ( clean ( sourceLine . slice ( 0 , column ) ) . length ) ; const pointerWidth = end - start ; const pointer = pointerPrefix + highlight ( '^' . repeat ( pointerWidth ) ) ; return header + '\n' + fileHeader + blankGutter + makeGutter ( line ) + printSourceLine + blankGutter + pointer + messageLine ; default : return header + '\n' + fileHeader + blankGutter + lines . map ( ( [ lineNumber , line ] ) => makeGutter ( lineNumber , true ) + clean ( line ) ) . join ( '' ) + blankGutter + messageLine ; } } function formatMessage ( message , margin , lineWidth ) { const finalLineWidth = Math . min ( lineWidth , 80 ) ; return message . split ( /\n+/ ) . map ( paragraph => { const lines = [ ] ; let line = margin ; const words = paragraph . split ( /[ \t]+/ ) ; for ( const word of words ) { if ( line . length + word . length + 1 > finalLineWidth ) { lines . push ( line ) ; line = margin + ' ' + word ; } else line += ' ' + word ; } if ( line !== margin ) lines . push ( line ) ; return lines . join ( '' ) ; } ) . join ( '\n' ) ; }