namespace FourSlash { import ArrayOrSingle = FourSlashInterface . ArrayOrSingle ; export const enum FourSlashTestType { Native , Shims , ShimsWithPreprocess , Server } interface FourSlashFile { content : string ; fileName : string ; symlinks ? : string [ ] ; version : number ; fileOptions : Harness . TestCaseParser . CompilerSettings ; } interface FourSlashData { globalOptions : Harness . TestCaseParser . CompilerSettings ; files : FourSlashFile [ ] ; symlinks : vfs . FileSet | undefined ; markerPositions : ts . ESMap < string , Marker > ; markers : Marker [ ] ; /**
         * Inserted in source files by surrounding desired text
         * in a range with `[|` and `|]`. For example,
         *
         * [|text in range|]
         *
         * is a range with `text in range` "selected".
         */ ranges : Range [ ] ; rangesByText ? : ts . MultiMap < string , Range > ; } export interface Marker { fileName : string ; position : number ; data ? : { } ; } export interface Range extends ts . TextRange { fileName : string ; marker ? : Marker ; } interface LocationInformation { position : number ; sourcePosition : number ; sourceLine : number ; sourceColumn : number ; } interface RangeLocationInformation extends LocationInformation { marker ? : Marker ; } interface ImplementationLocationInformation extends ts . ImplementationLocation { matched ? : boolean ; } export interface TextSpan { start : number ; end : number ; } const enum MetadataOptionNames { baselineFile = "baselinefile" , emitThisFile = "emitthisfile" , fileName = "filename" , resolveReference = "resolvereference" , symlink = "symlink" } const fileMetadataNames = [ MetadataOptionNames . fileName , MetadataOptionNames . emitThisFile , MetadataOptionNames . resolveReference , MetadataOptionNames . symlink ] ; function convertGlobalOptionsToCompilerOptions ( globalOptions ) { const settings = { target : ts . ScriptTarget . ES5 } ; Harness . Compiler . setCompilerOptionsFromHarnessSetting ( globalOptions , settings ) ; return settings ; } export class TestCancellationToken implements ts . HostCancellationToken { private static readonly notCanceled = - 1 ; private numberOfCallsBeforeCancellation = TestCancellationToken . notCanceled ; public isCancellationRequested ( ) { if ( this . numberOfCallsBeforeCancellation < 0 ) { return false ; } if ( this . numberOfCallsBeforeCancellation > 0 ) { this . numberOfCallsBeforeCancellation -- ; return false ; } return true ; } public setCancelled ( numberOfCalls = 0 ) { ts . Debug . assert ( numberOfCalls >= 0 ) ; this . numberOfCallsBeforeCancellation = numberOfCalls ; } public resetCancelled ( ) { this . numberOfCallsBeforeCancellation = TestCancellationToken . notCanceled ; } } export function verifyOperationIsCancelled ( f ) { try { f ( ) ; } catch ( e ) { if ( e instanceof ts . OperationCanceledException ) { return ; } } throw new Error ( "Operation should be cancelled" ) ; } export function ignoreInterpolations ( diagnostic ) { return { template : typeof diagnostic === "string" ? diagnostic : diagnostic . message } ; } function createScriptSnapShot ( sourceText ) { return ts . ScriptSnapshot . fromString ( sourceText ) ; } const enum CallHierarchyItemDirection { Root , Incoming , Outgoing } export class TestState { private languageServiceAdapterHost : Harness . LanguageService . LanguageServiceAdapterHost ; private languageService : ts . LanguageService ; private cancellationToken : TestCancellationToken ; private assertTextConsistent : ( ( fileName : string ) => void ) | undefined ; public currentCaretPosition = 0 ; public selectionEnd = - 1 ; public lastKnownMarker = "" ; public activeFile ! : FourSlashFile ; public enableFormatting = true ; public formatCodeSettings : ts . FormatCodeSettings ; private inputFiles = new ts . Map < string , string > ( ) ; private static getDisplayPartsJson ( displayParts ) { let result = "" ; ts . forEach ( displayParts , part => { if ( result ) { result += ",\n    " ; } else { result = "[\n    " ; } result += JSON . stringify ( part ) ; } ) ; if ( result ) { result += "\n]" ; } return result ; } private addMatchedInputFile ( referenceFilePath , extensions ) { const inputFiles = this . inputFiles ; const languageServiceAdapterHost = this . languageServiceAdapterHost ; const didAdd = tryAdd ( referenceFilePath ) ; if ( extensions && ! didAdd ) { ts . forEach ( extensions , ext => tryAdd ( referenceFilePath + ext ) ) ; } function tryAdd ( path ) { const inputFile = inputFiles . get ( path ) ; if ( inputFile && ! Harness . isDefaultLibraryFile ( path ) ) { languageServiceAdapterHost . addScript ( path , inputFile , true ) ; return true ; } } } private getLanguageServiceAdapter ( testType , cancellationToken , compilationOptions ) { switch ( testType ) { case FourSlashTestType . Native : return new Harness . LanguageService . NativeLanguageServiceAdapter ( cancellationToken , compilationOptions ) ; case FourSlashTestType . Shims : return new Harness . LanguageService . ShimLanguageServiceAdapter ( false , cancellationToken , compilationOptions ) ; case FourSlashTestType . ShimsWithPreprocess : return new Harness . LanguageService . ShimLanguageServiceAdapter ( true , cancellationToken , compilationOptions ) ; case FourSlashTestType . Server : return new Harness . LanguageService . ServerLanguageServiceAdapter ( cancellationToken , compilationOptions ) ; default : throw new Error ( "Unknown FourSlash test type: " ) ; } } constructor ( private originalInputFileName , private basePath , private testType , public testData ) { this . cancellationToken = new TestCancellationToken ( ) ; let compilationOptions = convertGlobalOptionsToCompilerOptions ( this . testData . globalOptions ) ; compilationOptions . skipDefaultLibCheck = true ; let startResolveFileRef ; let configFileName ; for ( const file of testData . files ) { this . inputFiles . set ( file . fileName , file . content ) ; if ( isConfig ( file ) ) { const configJson = ts . parseConfigFileTextToJson ( file . fileName , file . content ) ; if ( configJson . config === undefined ) { throw new Error ( `Failed to parse test ${ file . fileName }: ${ configJson . error ! . messageText }` ) ; } if ( configJson . config . compilerOptions ) { const baseDirectory = ts . normalizePath ( ts . getDirectoryPath ( file . fileName ) ) ; const tsConfig = ts . convertCompilerOptionsFromJson ( configJson . config . compilerOptions , baseDirectory , file . fileName ) ; if ( ! tsConfig . errors || ! tsConfig . errors . length ) { compilationOptions = ts . extend ( tsConfig . options , compilationOptions ) ; } } configFileName = file . fileName ; } if ( ! startResolveFileRef && file . fileOptions [ MetadataOptionNames . resolveReference ] === "true" ) { startResolveFileRef = file ; } else if ( startResolveFileRef ) { throw new Error ( "There exists a Fourslash file which has resolveReference flag specified; remove duplicated resolveReference flag" ) ; } } let configParseResult ; if ( configFileName ) { const baseDir = ts . normalizePath ( ts . getDirectoryPath ( configFileName ) ) ; const files = { [ baseDir ] : { } } ; this . inputFiles . forEach ( ( data , path ) => { const scriptInfo = new Harness . LanguageService . ScriptInfo ( path , undefined ! , false ) ; files [ path ] = new vfs . File ( data , { meta : { scriptInfo } } ) ; } ) ; const fs = new vfs . FileSystem ( true , { cwd : baseDir , files } ) ; const host = new fakes . ParseConfigHost ( fs ) ; const jsonSourceFile = ts . parseJsonText ( configFileName , this . inputFiles . get ( configFileName ) ! ) ; configParseResult = ts . parseJsonSourceFileConfigFileContent ( jsonSourceFile , host , baseDir , compilationOptions , configFileName ) ; compilationOptions = configParseResult . options ; } if ( compilationOptions . typeRoots ) { compilationOptions . typeRoots = compilationOptions . typeRoots . map ( p => ts . getNormalizedAbsolutePath ( p , this . basePath ) ) ; } const languageServiceAdapter = this . getLanguageServiceAdapter ( testType , this . cancellationToken , compilationOptions ) ; this . languageServiceAdapterHost = languageServiceAdapter . getHost ( ) ; this . languageService = memoWrap ( languageServiceAdapter . getLanguageService ( ) , this ) ; if ( this . testType === FourSlashTestType . Server ) { this . assertTextConsistent = fileName => ( languageServiceAdapter as Harness . LanguageService . ServerLanguageServiceAdapter ) . assertTextConsistent ( fileName ) ; } if ( startResolveFileRef ) { this . languageServiceAdapterHost . addScript ( startResolveFileRef . fileName , startResolveFileRef . content , true ) ; const resolvedResult = languageServiceAdapter . getPreProcessedFileInfo ( startResolveFileRef . fileName , startResolveFileRef . content ) ; const referencedFiles = resolvedResult . referencedFiles ; const importedFiles = resolvedResult . importedFiles ; ts . forEach ( referencedFiles , referenceFile => { const referenceFilePath = this . basePath + "/" + referenceFile . fileName ; this . addMatchedInputFile ( referenceFilePath , undefined ) ; } ) ; const exts = ts . flatten ( ts . getSupportedExtensions ( compilationOptions ) ) ; ts . forEach ( importedFiles , importedFile => { const importedFilePath = this . basePath + "/" + importedFile . fileName ; this . addMatchedInputFile ( importedFilePath , exts ) ; } ) ; if ( ! resolvedResult . isLibFile ) { this . languageServiceAdapterHost . addScript ( Harness . Compiler . defaultLibFileName , Harness . Compiler . getDefaultLibrarySourceFile ( ) ! . text , false ) ; compilationOptions . lib ?. forEach ( fileName => { const libFile = Harness . Compiler . getDefaultLibrarySourceFile ( fileName ) ; ts . Debug . assertIsDefined ( libFile , `Could not find lib file '${ fileName }'` ) ; if ( libFile ) { this . languageServiceAdapterHost . addScript ( fileName , libFile . text , false ) ; } } ) ; } } else { this . inputFiles . forEach ( ( file , fileName ) => { if ( ! Harness . isDefaultLibraryFile ( fileName ) ) { const isRootFile = ! configParseResult || ts . contains ( configParseResult . fileNames , fileName ) || ( ts . isDeclarationFileName ( fileName ) && ts . containsPath ( "/Library/Caches/typescript" , fileName ) ) ; this . languageServiceAdapterHost . addScript ( fileName , file , isRootFile ) ; } } ) ; if ( ! compilationOptions . noLib ) { const seen = new Set < string > ( ) ; const addSourceFile = ( fileName ) => { if ( seen . has ( fileName ) ) return ; seen . add ( fileName ) ; const libFile = Harness . Compiler . getDefaultLibrarySourceFile ( fileName ) ; ts . Debug . assertIsDefined ( libFile , `Could not find lib file '${ fileName }'` ) ; this . languageServiceAdapterHost . addScript ( fileName , libFile . text , false ) ; if ( ! ts . some ( libFile . libReferenceDirectives ) ) return ; for ( const directive of libFile . libReferenceDirectives ) { addSourceFile ( `lib.${ directive . fileName }.d.ts` ) ; } } ; addSourceFile ( Harness . Compiler . defaultLibFileName ) ; compilationOptions . lib ?. forEach ( addSourceFile ) ; } } for ( const file of testData . files ) { ts . forEach ( file . symlinks , link => { this . languageServiceAdapterHost . vfs . mkdirpSync ( vpath . dirname ( link ) ) ; this . languageServiceAdapterHost . vfs . symlinkSync ( file . fileName , link ) ; } ) ; } if ( testData . symlinks ) { this . languageServiceAdapterHost . vfs . apply ( testData . symlinks ) ; } this . formatCodeSettings = ts . testFormatSettings ; this . openFile ( 0 ) ; function memoWrap ( ls , target ) { const cacheableMembers = [ "getCompletionEntryDetails" , "getCompletionEntrySymbol" , "getQuickInfoAtPosition" , "getReferencesAtPosition" , "getDocumentHighlights" , ] ; const proxy = { } as ts . LanguageService ; const keys = ts . getAllKeys ( ls ) ; for ( const k of keys ) { const key = k as keyof typeof ls ; if ( cacheableMembers . indexOf ( key ) === - 1 ) { proxy [ key ] = ( ... args ) => ( ls [ key ] as Function ) ( ... args ) ; continue ; } const memo = Utils . memoize ( ( _version , _active , _caret , _selectEnd , _marker , ... args ) => ( ls [ key ] as Function ) ( ... args ) , ( ... args ) => args . map ( a => a && typeof a === "object" ? JSON . stringify ( a ) : a ) . join ( "|,|" ) ) ; proxy [ key ] = ( ... args ) => memo ( target . languageServiceAdapterHost . getScriptInfo ( target . activeFile . fileName ) ! . version , target . activeFile . fileName , target . currentCaretPosition , target . selectionEnd , target . lastKnownMarker , ... args ) ; } return proxy ; } } private getFileContent ( fileName ) { return ts . Debug . checkDefined ( this . tryGetFileContent ( fileName ) ) ; } private tryGetFileContent ( fileName ) { const script = this . languageServiceAdapterHost . getScriptInfo ( fileName ) ; return script && script . content ; } public goToMarker ( name = "" ) { const marker = ts . isString ( name ) ? this . getMarkerByName ( name ) : name ; if ( this . activeFile . fileName !== marker . fileName ) { this . openFile ( marker . fileName ) ; } const content = this . getFileContent ( marker . fileName ) ; if ( marker . position === - 1 || marker . position > content . length ) { throw new Error ( `Marker "${ name }" has been invalidated by unrecoverable edits to the file.` ) ; } const mName = ts . isString ( name ) ? name : this . markerName ( marker ) ; this . lastKnownMarker = mName ; this . goToPosition ( marker . position ) ; } public goToEachMarker ( markers , action ) { assert ( markers . length ) ; for ( let i = 0 ; i < markers . length ; i ++ ) { this . goToMarker ( markers [ i ] ) ; action ( markers [ i ] , i ) ; } } public goToEachRange ( action ) { const ranges = this . getRanges ( ) ; assert ( ranges . length ) ; for ( const range of ranges ) { this . selectRange ( range ) ; action ( range ) ; } } public markerName ( m ) { return ts . forEachEntry ( this . testData . markerPositions , ( marker , name ) => { if ( marker === m ) { return name ; } } ) ! ; } public goToPosition ( positionOrLineAndCharacter ) { const pos = typeof positionOrLineAndCharacter === "number" ? positionOrLineAndCharacter : this . languageServiceAdapterHost . lineAndCharacterToPosition ( this . activeFile . fileName , positionOrLineAndCharacter ) ; this . currentCaretPosition = pos ; this . selectionEnd = - 1 ; } public select ( startMarker , endMarker ) { const start = this . getMarkerByName ( startMarker ) , end = this . getMarkerByName ( endMarker ) ; ts . Debug . assert ( start . fileName === end . fileName ) ; if ( this . activeFile . fileName !== start . fileName ) { this . openFile ( start . fileName ) ; } this . goToPosition ( start . position ) ; this . selectionEnd = end . position ; } public selectAllInFile ( fileName ) { this . openFile ( fileName ) ; this . goToPosition ( 0 ) ; this . selectionEnd = this . activeFile . content . length ; } public selectRange ( range ) { this . goToRangeStart ( range ) ; this . selectionEnd = range . end ; } public selectLine ( index ) { const lineStart = this . languageServiceAdapterHost . lineAndCharacterToPosition ( this . activeFile . fileName , { line : index , character : 0 } ) ; const lineEnd = lineStart + this . getLineContent ( index ) . length ; this . selectRange ( { fileName : this . activeFile . fileName , pos : lineStart , end : lineEnd } ) ; } public moveCaretRight ( count = 1 ) { this . currentCaretPosition += count ; this . currentCaretPosition = Math . min ( this . currentCaretPosition , this . getFileContent ( this . activeFile . fileName ) . length ) ; this . selectionEnd = - 1 ; } public openFile ( indexOrName , content ? , scriptKindName ? ) { const fileToOpen = this . findFile ( indexOrName ) ; fileToOpen . fileName = ts . normalizeSlashes ( fileToOpen . fileName ) ; this . activeFile = fileToOpen ; this . languageServiceAdapterHost . openFile ( fileToOpen . fileName , content , scriptKindName ) ; } public verifyErrorExistsBetweenMarkers ( startMarkerName , endMarkerName , shouldExist ) { const startMarker = this . getMarkerByName ( startMarkerName ) ; const endMarker = this . getMarkerByName ( endMarkerName ) ; const predicate = ( errorMinChar , errorLimChar , startPos , endPos ) => ( ( errorMinChar === startPos ) && ( errorLimChar === endPos ) ) ? true : false ; const exists = this . anyErrorInRange ( predicate , startMarker , endMarker ) ; if ( exists !== shouldExist ) { this . printErrorLog ( shouldExist , this . getAllDiagnostics ( ) ) ; throw new Error ( `${ shouldExist ? "Expected" : "Did not expect" } failure between markers: '${ startMarkerName }', '${ endMarkerName }'` ) ; } } public verifyOrganizeImports ( newContent ) { const changes = this . languageService . organizeImports ( { fileName : this . activeFile . fileName , type : "file" } , this . formatCodeSettings , ts . emptyOptions ) ; this . applyChanges ( changes ) ; this . verifyFileContent ( this . activeFile . fileName , newContent ) ; } private raiseError ( message ) { throw new Error ( this . messageAtLastKnownMarker ( message ) ) ; } private messageAtLastKnownMarker ( message ) { const locationDescription = this . lastKnownMarker ? this . lastKnownMarker : this . getLineColStringAtPosition ( this . currentCaretPosition ) ; return `At ${ locationDescription }: ${ message }` ; } private assertionMessageAtLastKnownMarker ( msg ) { return "\nMarker: " + this . lastKnownMarker + "\nChecking: " + msg + "\n\n" ; } private getDiagnostics ( fileName , includeSuggestions = false ) { return [ ... this . languageService . getSyntacticDiagnostics ( fileName ) , ... this . languageService . getSemanticDiagnostics ( fileName ) , ... ( includeSuggestions ? this . languageService . getSuggestionDiagnostics ( fileName ) : ts . emptyArray ) , ] ; } private getAllDiagnostics ( ) { return ts . flatMap ( this . languageServiceAdapterHost . getFilenames ( ) , fileName => { if ( ! ts . isAnySupportedFileExtension ( fileName ) ) { return [ ] ; } const baseName = ts . getBaseFileName ( fileName ) ; if ( baseName === "package.json" || baseName === "tsconfig.json" || baseName === "jsconfig.json" ) { return [ ] ; } return this . getDiagnostics ( fileName ) ; } ) ; } public verifyErrorExistsAfterMarker ( markerName , shouldExist , after ) { const marker = this . getMarkerByName ( markerName ) ; let predicate ; if ( after ) { predicate = ( errorMinChar , errorLimChar , startPos ) => ( ( errorMinChar >= startPos ) && ( errorLimChar >= startPos ) ) ? true : false ; } else { predicate = ( errorMinChar , errorLimChar , startPos ) => ( ( errorMinChar <= startPos ) && ( errorLimChar <= startPos ) ) ? true : false ; } const exists = this . anyErrorInRange ( predicate , marker ) ; const diagnostics = this . getAllDiagnostics ( ) ; if ( exists !== shouldExist ) { this . printErrorLog ( shouldExist , diagnostics ) ; throw new Error ( `${ shouldExist ? "Expected" : "Did not expect" } failure at marker '${ markerName }'` ) ; } } private anyErrorInRange ( predicate , startMarker , endMarker ? ) { return this . getDiagnostics ( startMarker . fileName ) . some ( ( { start , length } ) => predicate ( start ! , start ! + length ! , startMarker . position , endMarker === undefined ? undefined : endMarker . position ) ) ; } private printErrorLog ( expectErrors , errors ) { if ( expectErrors ) { Harness . IO . log ( "Expected error not found.  Error list is:" ) ; } else { Harness . IO . log ( "Unexpected error(s) found.  Error list is:" ) ; } for ( const { start , length , messageText , file } of errors ) { Harness . IO . log ( "  " + this . formatRange ( file , start ! , length ! ) + ", message: " + ts . flattenDiagnosticMessageText ( messageText , Harness . IO . newLine ( ) ) + "\n" ) ; } } private formatRange ( file , start , length ) { if ( file ) { return `from: ${ this . formatLineAndCharacterOfPosition ( file , start ) }, to: ${ this . formatLineAndCharacterOfPosition ( file , start + length ) }` ; } return "global" ; } private formatLineAndCharacterOfPosition ( file , pos ) { if ( file ) { const { line , character } = ts . getLineAndCharacterOfPosition ( file , pos ) ; return `${ line }:${ character }` ; } return "global" ; } private formatPosition ( file , pos ) { if ( file ) { return file . fileName + "@" + pos ; } return "global" ; } public verifyNoErrors ( ) { ts . forEachKey ( this . inputFiles , fileName => { if ( ! ts . isAnySupportedFileExtension ( fileName ) || Harness . getConfigNameFromFileName ( fileName ) || this . testType !== FourSlashTestType . Server && ! ts . getAllowJSCompilerOption ( this . getProgram ( ) . getCompilerOptions ( ) ) && ! ts . resolutionExtensionIsTSOrJson ( ts . extensionFromPath ( fileName ) ) || ts . getBaseFileName ( fileName ) === "package.json" ) return ; const errors = this . getDiagnostics ( fileName ) . filter ( e => e . category !== ts . DiagnosticCategory . Suggestion ) ; if ( errors . length ) { this . printErrorLog ( false , errors ) ; const error = errors [ 0 ] ; const message = typeof error . messageText === "string" ? error . messageText : error . messageText . messageText ; this . raiseError ( `Found an error: ${ this . formatPosition ( error . file ! , error . start ! ) }: ${ message }` ) ; } } ) ; } public verifyErrorExistsAtRange ( range , code , expectedMessage ? ) { const span = ts . createTextSpanFromRange ( range ) ; const hasMatchingError = ts . some ( this . getDiagnostics ( range . fileName ) , ( { code , messageText , start , length } ) => code === code && ( ! expectedMessage || expectedMessage === messageText ) && ts . isNumber ( start ) && ts . isNumber ( length ) && ts . textSpansEqual ( span , { start , length } ) ) ; if ( ! hasMatchingError ) { this . raiseError ( `No error with code ${ code } found at provided range.` ) ; } } public verifyNumberOfErrorsInCurrentFile ( expected ) { const errors = this . getDiagnostics ( this . activeFile . fileName ) ; const actual = errors . length ; if ( actual !== expected ) { this . printErrorLog ( false , errors ) ; const errorMsg = "Actual number of errors (" + actual + ") does not match expected number (" + expected + ")" ; Harness . IO . log ( errorMsg ) ; this . raiseError ( errorMsg ) ; } } public verifyEval ( expr , value ) { const emit = this . languageService . getEmitOutput ( this . activeFile . fileName ) ; if ( emit . outputFiles . length !== 1 ) { throw new Error ( "Expected exactly one output from emit of " + this . activeFile . fileName ) ; } const evaluation = new Function ( `${ emit . outputFiles [ 0 ] . text };\r\nreturn (${ expr });` ) ( ) ; if ( evaluation !== value ) { this . raiseError ( `Expected evaluation of expression "${ expr }" to equal "${ value }", but got "${ evaluation }"` ) ; } } public verifyGoToDefinitionIs ( endMarker ) { this . verifyGoToXWorker ( undefined , toArray ( endMarker ) , ( ) => this . getGoToDefinition ( ) ) ; } public verifyGoToDefinition ( arg0 , endMarkerNames ? ) { this . verifyGoToX ( arg0 , endMarkerNames , ( ) => this . getGoToDefinitionAndBoundSpan ( ) ) ; } private getGoToDefinition ( ) { return this . languageService . getDefinitionAtPosition ( this . activeFile . fileName , this . currentCaretPosition ) ! ; } private getGoToDefinitionAndBoundSpan ( ) { return this . languageService . getDefinitionAndBoundSpan ( this . activeFile . fileName , this . currentCaretPosition ) ! ; } public verifyGoToType ( arg0 , endMarkerNames ? ) { this . verifyGoToX ( arg0 , endMarkerNames , ( ) => this . languageService . getTypeDefinitionAtPosition ( this . activeFile . fileName , this . currentCaretPosition ) ) ; } private verifyGoToX ( arg0 , endMarkerNames , getDefs ) { if ( endMarkerNames ) { this . verifyGoToXPlain ( arg0 , endMarkerNames , getDefs ) ; } else if ( ts . isArray ( arg0 ) ) { const pairs = arg0 as readonly [ ArrayOrSingle < string > , ArrayOrSingle < string > ] [ ] ; for ( const [ start , end ] of pairs ) { this . verifyGoToXPlain ( start , end , getDefs ) ; } } else { const obj = arg0 ; for ( const startMarkerName in obj ) { if ( ts . hasProperty ( obj , startMarkerName ) ) { this . verifyGoToXPlain ( startMarkerName , obj [ startMarkerName ] , getDefs ) ; } } } } private verifyGoToXPlain ( startMarkerNames , endMarkerNames , getDefs ) { for ( const start of toArray ( startMarkerNames ) ) { this . verifyGoToXSingle ( start , endMarkerNames , getDefs ) ; } } public verifyGoToDefinitionForMarkers ( markerNames ) { for ( const markerName of markerNames ) { this . verifyGoToXSingle ( `${ markerName }Reference` , `${ markerName }Definition` , ( ) => this . getGoToDefinition ( ) ) ; } } private verifyGoToXSingle ( startMarkerName , endMarkerNames , getDefs ) { this . goToMarker ( startMarkerName ) ; this . verifyGoToXWorker ( startMarkerName , toArray ( endMarkerNames ) , getDefs , startMarkerName ) ; } private verifyGoToXWorker ( startMarker , endMarkers , getDefs , startMarkerName ? ) { const defs = getDefs ( ) ; let definitions ; let testName ; if ( ! defs || ts . isArray ( defs ) ) { definitions = defs as ts . DefinitionInfo [ ] || [ ] ; testName = "goToDefinitions" ; } else { this . verifyDefinitionTextSpan ( defs , startMarkerName ! ) ; definitions = defs . definitions ! ; testName = "goToDefinitionsAndBoundSpan" ; } if ( endMarkers . length !== definitions . length ) { this . raiseError ( `${ testName } failed - expected to find ${ endMarkers . length } definitions but got ${ definitions . length }` ) ; } ts . zipWith ( endMarkers , definitions , ( endMarkerOrFileResult , definition , i ) => { const markerName = typeof endMarkerOrFileResult === "string" ? endMarkerOrFileResult : endMarkerOrFileResult . marker ; const marker = markerName !== undefined ? this . getMarkerByName ( markerName ) : undefined ; const expectedFileName = marker ?. fileName || typeof endMarkerOrFileResult !== "string" && endMarkerOrFileResult . file ; ts . Debug . assert ( typeof expectedFileName === "string" ) ; const expectedPosition = marker ?. position || 0 ; if ( ts . comparePaths ( expectedFileName , definition . fileName , true ) !== ts . Comparison . EqualTo || expectedPosition !== definition . textSpan . start ) { const filesToDisplay = ts . deduplicate ( [ expectedFileName , definition . fileName ] , ts . equateValues ) ; const markers = [ { text : "EXPECTED" , fileName : expectedFileName , position : expectedPosition } , { text : "ACTUAL" , fileName : definition . fileName , position : definition . textSpan . start } ] ; const text = filesToDisplay . map ( fileName => { const markersToRender = markers . filter ( m => m . fileName === fileName ) . sort ( ( a , b ) => b . position - a . position ) ; let fileContent = this . tryGetFileContent ( fileName ) || "" ; for ( const marker of markersToRender ) { fileContent = fileContent . slice ( 0 , marker . position ) + `\x1b[1;4m/*${ marker . text }*/\x1b[0;31m` + fileContent . slice ( marker . position ) ; } return `// @Filename: ${ fileName }\n${ fileContent }` ; } ) . join ( "\n\n" ) ; this . raiseError ( `${ testName } failed for definition ${ markerName || expectedFileName } (${ i }): expected ${ expectedFileName } at ${ expectedPosition }, got ${ definition . fileName } at ${ definition . textSpan . start }\n\n${ text }\n` ) ; } if ( definition . unverified && ( typeof endMarkerOrFileResult === "string" || ! endMarkerOrFileResult . unverified ) ) { const isFileResult = typeof endMarkerOrFileResult !== "string" && ! ! endMarkerOrFileResult . file ; this . raiseError ( `${ testName } failed for definition ${ markerName || expectedFileName } (${ i }): The actual definition was an \`unverified\` result. Use:\n\n` + `    verify.goToDefinition(${ startMarker === undefined ? "startMarker" : `"${ startMarker }"` }, { ${ isFileResult ? `file: "${ expectedFileName }"` : `marker: "${ markerName }"` }, unverified: true })\n\n` + `if this is expected.` ) ; } } ) ; } private verifyDefinitionTextSpan ( defs , startMarkerName ) { const range = this . testData . ranges . find ( range => this . markerName ( range . marker ! ) === startMarkerName ) ; if ( ! range && ! defs . textSpan ) { return ; } if ( ! range ) { const marker = this . getMarkerByName ( startMarkerName ) ; const startFile = marker . fileName ; const fileContent = this . getFileContent ( startFile ) ; const spanContent = fileContent . slice ( defs . textSpan . start , ts . textSpanEnd ( defs . textSpan ) ) ; const spanContentWithMarker = spanContent . slice ( 0 , marker . position - defs . textSpan . start ) + `/*${ startMarkerName }*/` + spanContent . slice ( marker . position - defs . textSpan . start ) ; const suggestedFileContent = ( fileContent . slice ( 0 , defs . textSpan . start ) + `\x1b[1;4m[|${ spanContentWithMarker }|]\x1b[0;31m` + fileContent . slice ( ts . textSpanEnd ( defs . textSpan ) ) ) . split ( /\r?\n/ ) . map ( line => " " . repeat ( 6 ) + line ) . join ( ts . sys . newLine ) ; this . raiseError ( `goToDefinitionsAndBoundSpan failed. Found a starting TextSpan around '${ spanContent }' in '${ startFile }' (at position ${ defs . textSpan . start }). ` + `If this is the correct input span, put a fourslash range around it: \n\n${ suggestedFileContent }\n` ) ; } else { this . assertTextSpanEqualsRange ( defs . textSpan , range , "goToDefinitionsAndBoundSpan failed" ) ; } } public verifyGetEmitOutputForCurrentFile ( expected ) { const emit = this . languageService . getEmitOutput ( this . activeFile . fileName ) ; if ( emit . outputFiles . length !== 1 ) { throw new Error ( "Expected exactly one output from emit of " + this . activeFile . fileName ) ; } const actual = emit . outputFiles [ 0 ] . text ; if ( actual !== expected ) { this . raiseError ( `Expected emit output to be "${ expected }", but got "${ actual }"` ) ; } } public verifyGetEmitOutputContentsForCurrentFile ( expected ) { const emit = this . languageService . getEmitOutput ( this . activeFile . fileName ) ; assert . equal ( emit . outputFiles . length , expected . length , "Number of emit output files" ) ; ts . zipWith ( emit . outputFiles , expected , ( outputFile , expected ) => { assert . equal ( outputFile . name , expected . name , "FileName" ) ; assert . equal ( outputFile . text , expected . text , "Content" ) ; } ) ; } public verifyInlayHints ( expected , span = { start : 0 , length : this . activeFile . content . length } , preference ? ) { const hints = this . languageService . provideInlayHints ( this . activeFile . fileName , span , preference ) ; assert . equal ( hints . length , expected . length , "Number of hints" ) ; const sortHints = ( a , b ) => { return a . position - b . position ; } ; ts . zipWith ( hints . sort ( sortHints ) , [ ... expected ] . sort ( sortHints ) , ( actual , expected ) => { assert . equal ( actual . text , expected . text , "Text" ) ; assert . equal ( actual . position , expected . position , "Position" ) ; assert . equal ( actual . kind , expected . kind , "Kind" ) ; assert . equal ( actual . whitespaceBefore , expected . whitespaceBefore , "whitespaceBefore" ) ; assert . equal ( actual . whitespaceAfter , expected . whitespaceAfter , "whitespaceAfter" ) ; } ) ; } public verifyCompletions ( options ) { if ( options . marker === undefined ) { this . verifyCompletionsWorker ( options ) ; } else { for ( const marker of toArray ( options . marker ) ) { this . goToMarker ( marker ) ; this . verifyCompletionsWorker ( options ) ; } } } private verifyCompletionsWorker ( options ) { const actualCompletions = this . getCompletionListAtCaret ( { ... options . preferences , triggerCharacter : options . triggerCharacter } ) ! ; if ( ! actualCompletions ) { if ( ts . hasProperty ( options , "exact" ) && ( options . exact === undefined || ts . isArray ( options . exact ) && ! options . exact . length ) ) { return ; } this . raiseError ( `No completions at position '${ this . currentCaretPosition }'.` ) ; } if ( actualCompletions . isNewIdentifierLocation !== ( options . isNewIdentifierLocation || false ) ) { this . raiseError ( `Expected 'isNewIdentifierLocation' to be ${ options . isNewIdentifierLocation || false }, got ${ actualCompletions . isNewIdentifierLocation }` ) ; } if ( ts . hasProperty ( options , "isGlobalCompletion" ) && actualCompletions . isGlobalCompletion !== options . isGlobalCompletion ) { this . raiseError ( `Expected 'isGlobalCompletion to be ${ options . isGlobalCompletion }, got ${ actualCompletions . isGlobalCompletion }` ) ; } if ( ts . hasProperty ( options , "optionalReplacementSpan" ) ) { assert . deepEqual ( actualCompletions . optionalReplacementSpan && actualCompletions . optionalReplacementSpan , options . optionalReplacementSpan && ts . createTextSpanFromRange ( options . optionalReplacementSpan ) , "Expected 'optionalReplacementSpan' properties to match" ) ; } const nameToEntries = new ts . Map < string , ts . CompletionEntry [ ] > ( ) ; for ( const entry of actualCompletions . entries ) { const entries = nameToEntries . get ( entry . name ) ; if ( ! entries ) { nameToEntries . set ( entry . name , [ entry ] ) ; } else { if ( entries . some ( e => e . source === entry . source && e . data ?. exportName === entry . data ?. exportName && e . data ?. fileName === entry . data ?. fileName && e . data ?. moduleSpecifier === entry . data ?. moduleSpecifier && e . data ?. ambientModuleName === entry . data ?. ambientModuleName ) ) { this . raiseError ( `Duplicate completions for ${ entry . name }` ) ; } entries . push ( entry ) ; } } if ( ts . hasProperty ( options , "exact" ) ) { ts . Debug . assert ( ! ts . hasProperty ( options , "includes" ) && ! ts . hasProperty ( options , "excludes" ) && ! ts . hasProperty ( options , "unsorted" ) ) ; if ( options . exact === undefined ) throw this . raiseError ( "Expected no completions" ) ; this . verifyCompletionsAreExactly ( actualCompletions . entries , options . exact , options . marker ) ; } else if ( options . unsorted ) { ts . Debug . assert ( ! ts . hasProperty ( options , "includes" ) && ! ts . hasProperty ( options , "excludes" ) ) ; for ( const expectedEntry of options . unsorted ) { const name = typeof expectedEntry === "string" ? expectedEntry : expectedEntry . name ; const found = nameToEntries . get ( name ) ; if ( ! found ) throw this . raiseError ( `Unsorted: completion '${ name }' not found.` ) ; if ( ! found . length ) throw this . raiseError ( `Unsorted: no completions with name '${ name }' remain unmatched.` ) ; this . verifyCompletionEntry ( found . shift ( ) ! , expectedEntry ) ; } if ( actualCompletions . entries . length !== options . unsorted . length ) { const unmatched = [ ] ; nameToEntries . forEach ( entries => { unmatched . push ( ... entries . map ( e => e . name ) ) ; } ) ; this . raiseError ( `Additional completions found not included in 'unsorted': ${ unmatched . join ( "\n" ) }` ) ; } } else { if ( options . includes ) { for ( const include of toArray ( options . includes ) ) { const name = typeof include === "string" ? include : include . name ; const found = nameToEntries . get ( name ) ; if ( ! found ) throw this . raiseError ( `Includes: completion '${ name }' not found.` ) ; if ( ! found . length ) throw this . raiseError ( `Includes: no completions with name '${ name }' remain unmatched.` ) ; this . verifyCompletionEntry ( found . shift ( ) ! , include ) ; } } if ( options . excludes ) { for ( const exclude of toArray ( options . excludes ) ) { assert ( typeof exclude === "string" ) ; if ( nameToEntries . has ( exclude ) ) { this . raiseError ( `Excludes: unexpected completion '${ exclude }' found.` ) ; } } } } } private verifyCompletionEntry ( actual , expected ) { expected = typeof expected === "string" ? { name : expected } : expected ; if ( actual . insertText !== expected . insertText ) { this . raiseError ( `Completion insert text did not match: ${ showTextDiff ( expected . insertText || "" , actual . insertText || "" ) }` ) ; } const convertedReplacementSpan = expected . replacementSpan && ts . createTextSpanFromRange ( expected . replacementSpan ) ; if ( convertedReplacementSpan ?. length ) { try { assert . deepEqual ( actual . replacementSpan , convertedReplacementSpan ) ; } catch { this . raiseError ( `Expected completion replacementSpan to be ${ stringify ( convertedReplacementSpan ) }, got ${ stringify ( actual . replacementSpan ) }` ) ; } } if ( expected . kind !== undefined || expected . kindModifiers !== undefined ) { assert . equal ( actual . kind , expected . kind , `Expected 'kind' for ${ actual . name } to match` ) ; assert . equal ( actual . kindModifiers , expected . kindModifiers || "" , `Expected 'kindModifiers' for ${ actual . name } to match` ) ; } if ( expected . isFromUncheckedFile !== undefined ) { assert . equal < boolean | undefined > ( actual . isFromUncheckedFile , expected . isFromUncheckedFile , "Expected 'isFromUncheckedFile' properties to match" ) ; } if ( expected . isPackageJsonImport !== undefined ) { assert . equal < boolean | undefined > ( actual . isPackageJsonImport , expected . isPackageJsonImport , "Expected 'isPackageJsonImport' properties to match" ) ; } assert . equal ( actual . hasAction , expected . hasAction , `Expected 'hasAction' properties to match` ) ; assert . equal ( actual . isRecommended , expected . isRecommended , `Expected 'isRecommended' properties to match'` ) ; assert . equal ( actual . isSnippet , expected . isSnippet , `Expected 'isSnippet' properties to match` ) ; assert . equal ( actual . source , expected . source , `Expected 'source' values to match` ) ; assert . equal ( actual . sortText , expected . sortText || ts . Completions . SortText . LocationPriority , `Expected 'sortText' properties to match` ) ; if ( expected . sourceDisplay && actual . sourceDisplay ) { assert . equal ( ts . displayPartsToString ( actual . sourceDisplay ) , expected . sourceDisplay , `Expected 'sourceDisplay' properties to match` ) ; } if ( expected . text !== undefined ) { const actualDetails = ts . Debug . checkDefined ( this . getCompletionEntryDetails ( actual . name , actual . source , actual . data ) , `No completion details available for name '${ actual . name }' and source '${ actual . source }'` ) ; assert . equal ( ts . displayPartsToString ( actualDetails . displayParts ) , expected . text , "Expected 'text' property to match 'displayParts' string" ) ; assert . equal ( ts . displayPartsToString ( actualDetails . documentation ) , expected . documentation || "" , "Expected 'documentation' property to match 'documentation' display parts string" ) ; assert . equal ( actualDetails . kindModifiers , actual . kindModifiers , "Expected 'kindModifiers' properties to match" ) ; assert . equal ( actualDetails . source && ts . displayPartsToString ( actualDetails . source ) , expected . sourceDisplay , "Expected 'sourceDisplay' property to match 'source' display parts string" ) ; if ( ! actual . sourceDisplay ) { assert . equal ( actualDetails . sourceDisplay && ts . displayPartsToString ( actualDetails . sourceDisplay ) , expected . sourceDisplay , "Expected 'sourceDisplay' property to match 'sourceDisplay' display parts string" ) ; } assert . deepEqual ( actualDetails . tags , expected . tags ) ; } else { assert ( expected . documentation === undefined && expected . tags === undefined , "If specifying completion details, should specify 'text'" ) ; } } private verifyCompletionsAreExactly ( actual , expected , marker ? ) { if ( ! ts . isArray ( expected ) ) { expected = [ expected ] ; } assert . deepEqual ( actual . map ( a => a . name ) , expected . map ( e => typeof e === "string" ? e : e . name ) , marker ? "At marker " + JSON . stringify ( marker ) : undefined ) ; ts . zipWith ( actual , expected , ( completion , expectedCompletion , index ) => { const name = typeof expectedCompletion === "string" ? expectedCompletion : expectedCompletion . name ; if ( completion . name !== name ) { this . raiseError ( `${ marker ? JSON . stringify ( marker ) : "" } Expected completion at index ${ index } to be ${ name }, got ${ completion . name }` ) ; } this . verifyCompletionEntry ( completion , expectedCompletion ) ; } ) ; const { plusArgument , plusFunctionName } = expected as FourSlashInterface . ExpectedExactCompletionsPlus ; if ( plusArgument ) { assert . deepEqual ( plusArgument , expected . filter ( entry => plusArgument . includes ( entry ) ) , `At marker ${ JSON . stringify ( marker ) }: Argument to '${ plusFunctionName }' was incorrectly sorted.` ) ; } } /** Use `getProgram` instead of accessing this directly. */ private _program : ts . Program | undefined | "missing" ; /** Use `getChecker` instead of accessing this directly. */ private _checker : ts . TypeChecker | undefined ; private getProgram ( ) { if ( ! this . _program ) this . _program = this . languageService . getProgram ( ) || "missing" ; if ( this . _program === "missing" ) ts . Debug . fail ( "Could not retrieve program from language service" ) ; return this . _program ; } private getChecker ( ) { return this . _checker || ( this . _checker = this . getProgram ( ) . getTypeChecker ( ) ) ; } private getSourceFile ( ) { const { fileName } = this . activeFile ; const result = this . getProgram ( ) . getSourceFile ( fileName ) ; if ( ! result ) { throw new Error ( `Could not get source file ${ fileName }` ) ; } return result ; } private getNode ( ) { return ts . getTouchingPropertyName ( this . getSourceFile ( ) , this . currentCaretPosition ) ; } private goToAndGetNode ( range ) { this . goToRangeStart ( range ) ; const node = this . getNode ( ) ; this . verifyRange ( "touching property name" , range , node ) ; return node ; } private verifyRange ( desc , expected , actual ) { const actualStart = actual . getStart ( ) ; const actualEnd = actual . getEnd ( ) ; if ( actualStart !== expected . pos || actualEnd !== expected . end ) { this . raiseError ( `${ desc } should be ${ expected . pos }-${ expected . end }, got ${ actualStart }-${ actualEnd }` ) ; } } private verifySymbol ( symbol , declarationRanges ) { const { declarations } = symbol ; if ( declarations ?. length !== declarationRanges . length ) { this . raiseError ( `Expected to get ${ declarationRanges . length } declarations, got ${ declarations ?. length }` ) ; } ts . zipWith ( declarations , declarationRanges , ( decl , range ) => { this . verifyRange ( "symbol declaration" , range , decl ) ; } ) ; } public verifySymbolAtLocation ( startRange , declarationRanges ) { const node = this . goToAndGetNode ( startRange ) ; const symbol = this . getChecker ( ) . getSymbolAtLocation ( node ) ! ; if ( ! symbol ) { this . raiseError ( "Could not get symbol at location" ) ; } this . verifySymbol ( symbol , declarationRanges ) ; } public symbolsInScope ( range ) { const node = this . goToAndGetNode ( range ) ; return this . getChecker ( ) . getSymbolsInScope ( node , ts . SymbolFlags . Value | ts . SymbolFlags . Type | ts . SymbolFlags . Namespace ) ; } public setTypesRegistry ( map ) { this . languageServiceAdapterHost . typesRegistry = new ts . Map ( ts . getEntries ( map ) ) ; } public verifyTypeOfSymbolAtLocation ( range , symbol , expected ) { const node = this . goToAndGetNode ( range ) ; const checker = this . getChecker ( ) ; const type = checker . getTypeOfSymbolAtLocation ( symbol , node ) ; const actual = checker . typeToString ( type ) ; if ( actual !== expected ) { this . raiseError ( displayExpectedAndActualString ( expected , actual ) ) ; } } private verifyDocumentHighlightsRespectFilesList ( files ) { const startFile = this . activeFile . fileName ; for ( const fileName of files ) { const searchFileNames = startFile === fileName ? [ startFile ] : [ startFile , fileName ] ; const highlights = this . getDocumentHighlightsAtCurrentPosition ( searchFileNames ) ; if ( highlights && ! highlights . every ( dh => ts . contains ( searchFileNames , dh . fileName ) ) ) { this . raiseError ( `When asking for document highlights only in files ${ searchFileNames }, got document highlights in ${ unique ( highlights , dh => dh . fileName ) }` ) ; } } } public verifyReferenceGroups ( starts , parts ) { interface ReferenceGroupJson { definition : string | { text : string ; range : ts . TextSpan ; } ; references : ts . ReferenceEntry [ ] ; } interface RangeMarkerData { id ? : string ; isWriteAccess ? : boolean ; isDefinition ? : boolean ; isInString ? : true ; contextRangeIndex ? : number ; contextRangeDelta ? : number ; contextRangeId ? : string ; } const fullExpected = ts . map < FourSlashInterface . ReferenceGroup , ReferenceGroupJson > ( parts , ( { definition , ranges } ) => ( { definition : typeof definition === "string" ? definition : { ... definition , range : ts . createTextSpanFromRange ( definition . range ) } , references : ranges . map < ts . ReferenceEntry > ( r => { const { isWriteAccess = false , isDefinition = false , isInString , contextRangeIndex , contextRangeDelta , contextRangeId } = ( r . marker && r . marker . data || { } ) as RangeMarkerData ; let contextSpan ; if ( contextRangeDelta !== undefined ) { const allRanges = this . getRanges ( ) ; const index = allRanges . indexOf ( r ) ; if ( index !== - 1 ) { contextSpan = ts . createTextSpanFromRange ( allRanges [ index + contextRangeDelta ] ) ; } } else if ( contextRangeId !== undefined ) { const allRanges = this . getRanges ( ) ; const contextRange = ts . find ( allRanges , range => ( range . marker ?. data as RangeMarkerData ) ?. id === contextRangeId ) ; if ( contextRange ) { contextSpan = ts . createTextSpanFromRange ( contextRange ) ; } } else if ( contextRangeIndex !== undefined ) { contextSpan = ts . createTextSpanFromRange ( this . getRanges ( ) [ contextRangeIndex ] ) ; } return { textSpan : ts . createTextSpanFromRange ( r ) , fileName : r . fileName , ... ( contextSpan ? { contextSpan } : undefined ) , isWriteAccess , isDefinition , ... ( isInString ? { isInString : true } : undefined ) , } ; } ) , } ) ) ; for ( const start of toArray < string | Range > ( starts ) ) { this . goToMarkerOrRange ( start ) ; const fullActual = ts . map < ts . ReferencedSymbol , ReferenceGroupJson > ( this . findReferencesAtCaret ( ) , ( { definition , references } , i ) => { const text = definition . displayParts . map ( d => d . text ) . join ( "" ) ; return { definition : fullExpected . length > i && typeof fullExpected [ i ] . definition === "string" ? text : { text , range : definition . textSpan } , references , } ; } ) ; this . assertObjectsEqual ( fullActual , fullExpected ) ; if ( parts ) { this . verifyDocumentHighlightsRespectFilesList ( unique ( ts . flatMap ( parts , p => p . ranges ) , r => r . fileName ) ) ; } } } public verifyBaselineFindAllReferences ( ... markerNames ) { ts . Debug . assert ( markerNames . length > 0 , "Must pass at least one marker name to `baselineFindAllReferences()`" ) ; const baseline = markerNames . map ( markerName => { this . goToMarker ( markerName ) ; const marker = this . getMarkerByName ( markerName ) ; const references = this . languageService . findReferences ( marker . fileName , marker . position ) ; const refsByFile = references ? ts . group ( ts . sort ( ts . flatMap ( references , r => r . references ) , ( a , b ) => a . textSpan . start - b . textSpan . start ) , ref => ref . fileName ) : ts . emptyArray ; const baselineContent = this . getBaselineContentForGroupedReferences ( refsByFile , markerName ) ; return baselineContent + JSON . stringify ( references , undefined , 2 ) ; } ) . join ( "\n\n" ) ; Harness . Baseline . runBaseline ( this . getBaselineFileNameForContainingTestFile ( ".baseline.jsonc" ) , baseline ) ; } public verifyBaselineGetFileReferences ( fileName ) { const references = this . languageService . getFileReferences ( fileName ) ; const refsByFile = references ? ts . group ( ts . sort ( references , ( a , b ) => a . textSpan . start - b . textSpan . start ) , ref => ref . fileName ) : ts . emptyArray ; let baselineContent = this . getBaselineContentForGroupedReferences ( refsByFile ) ; baselineContent += JSON . stringify ( references , undefined , 2 ) ; Harness . Baseline . runBaseline ( this . getBaselineFileNameForContainingTestFile ( ".baseline.jsonc" ) , baselineContent ) ; } private getBaselineContentForGroupedReferences ( refsByFile , markerName ? ) { const marker = markerName !== undefined ? this . getMarkerByName ( markerName ) : undefined ; let baselineContent = "" ; for ( const group of refsByFile ) { baselineContent += getBaselineContentForFile ( group [ 0 ] . fileName , this . getFileContent ( group [ 0 ] . fileName ) ) ; baselineContent += "\n\n" ; } return baselineContent ; function getBaselineContentForFile ( fileName , content ) { let newContent = `=== ${ fileName } ===\n` ; let pos = 0 ; for ( const { textSpan } of refsByFile . find ( refs => refs [ 0 ] . fileName === fileName ) ?? ts . emptyArray ) { const end = textSpan . start + textSpan . length ; if ( fileName === marker ?. fileName && pos <= marker . position && marker . position < textSpan . start ) { newContent += content . slice ( pos , marker . position ) ; newContent += "/*FIND ALL REFS*/" ; pos = marker . position ; } newContent += content . slice ( pos , textSpan . start ) ; pos = textSpan . start ; if ( fileName === marker ?. fileName && marker . position === textSpan . start ) { newContent += "/*FIND ALL REFS*/" ; newContent += "[|" ; } else if ( fileName === marker ?. fileName && ts . textSpanContainsPosition ( textSpan , marker . position ) ) { newContent += "[|" ; newContent += content . slice ( pos , marker . position ) ; newContent += "/*FIND ALL REFS*/" ; pos = marker . position ; } else { newContent += "[|" ; } newContent += content . slice ( pos , end ) ; newContent += "|]" ; pos = end ; } if ( marker ?. fileName === fileName && marker . position >= pos ) { newContent += content . slice ( pos , marker . position ) ; newContent += "/*FIND ALL REFS*/" ; pos = marker . position ; } newContent += content . slice ( pos ) ; return newContent . split ( /\r?\n/ ) . map ( l => "// " + l ) . join ( "\n" ) ; } } public verifySingleReferenceGroup ( definition , ranges ? ) { ranges = ts . isString ( ranges ) ? this . rangesByText ( ) . get ( ranges ) ! : ranges || this . getRanges ( ) ; this . verifyReferenceGroups ( ranges , [ { definition , ranges } ] ) ; } private assertObjectsEqual < T > ( fullActual , fullExpected , msgPrefix = "" ) { const recur = < U > ( actual , expected , path ) => { const fail = ( msg ) => { this . raiseError ( `${ msgPrefix } At ${ path }: ${ msg } ${ displayExpectedAndActualString ( stringify ( fullExpected ) , stringify ( fullActual ) ) }` ) ; } ; if ( ( actual === undefined ) !== ( expected === undefined ) ) { fail ( `Expected ${ stringify ( expected ) }, got ${ stringify ( actual ) }` ) ; } for ( const key in actual ) { if ( ts . hasProperty ( actual as any , key ) ) { const ak = actual [ key ] , ek = expected [ key ] ; if ( typeof ak === "object" && typeof ek === "object" ) { recur ( ak , ek , path ? path + "." + key : key ) ; } else if ( ak !== ek ) { fail ( `Expected '${ key }' to be '${ stringify ( ek ) }', got '${ stringify ( ak ) }'` ) ; } } } for ( const key in expected ) { if ( ts . hasProperty ( expected as any , key ) ) { if ( ! ts . hasProperty ( actual as any , key ) ) { fail ( `${ msgPrefix }Missing property '${ key }'` ) ; } } } } ; if ( fullActual === undefined || fullExpected === undefined ) { if ( fullActual === fullExpected ) { return ; } this . raiseError ( `${ msgPrefix } ${ displayExpectedAndActualString ( stringify ( fullExpected ) , stringify ( fullActual ) ) }` ) ; } recur ( fullActual , fullExpected , "" ) ; } public verifyDisplayPartsOfReferencedSymbol ( expected ) { const referencedSymbols = this . findReferencesAtCaret ( ) ! ; if ( referencedSymbols . length === 0 ) { this . raiseError ( "No referenced symbols found at current caret position" ) ; } else if ( referencedSymbols . length > 1 ) { this . raiseError ( "More than one referenced symbol found" ) ; } assert . equal ( TestState . getDisplayPartsJson ( referencedSymbols [ 0 ] . definition . displayParts ) , TestState . getDisplayPartsJson ( expected ) , this . messageAtLastKnownMarker ( "referenced symbol definition display parts" ) ) ; } private configure ( preferences ) { if ( this . testType === FourSlashTestType . Server ) { ( this . languageService as ts . server . SessionClient ) . configure ( preferences ) ; } } private getCompletionListAtCaret ( options ? ) { if ( options ) { this . configure ( options ) ; } return this . languageService . getCompletionsAtPosition ( this . activeFile . fileName , this . currentCaretPosition , options , this . formatCodeSettings ) ; } private getCompletionEntryDetails ( entryName , source , data , preferences ? ) { if ( preferences ) { this . configure ( preferences ) ; } return this . languageService . getCompletionEntryDetails ( this . activeFile . fileName , this . currentCaretPosition , entryName , this . formatCodeSettings , source , preferences , data ) ; } private findReferencesAtCaret ( ) { return this . languageService . findReferences ( this . activeFile . fileName , this . currentCaretPosition ) ; } public getSyntacticDiagnostics ( expected ) { const diagnostics = this . languageService . getSyntacticDiagnostics ( this . activeFile . fileName ) ; this . testDiagnostics ( expected , diagnostics , "error" ) ; } public getSemanticDiagnostics ( expected ) { const diagnostics = this . languageService . getSemanticDiagnostics ( this . activeFile . fileName ) ; this . testDiagnostics ( expected , diagnostics , "error" ) ; } public getSuggestionDiagnostics ( expected ) { this . testDiagnostics ( expected , this . languageService . getSuggestionDiagnostics ( this . activeFile . fileName ) , "suggestion" ) ; } private testDiagnostics ( expected , diagnostics , category ) { assert . deepEqual ( ts . realizeDiagnostics ( diagnostics , "\n" ) , expected . map ( ( e ) : ts . RealizedDiagnostic => { const range = e . range || this . getRangesInFile ( ) [ 0 ] ; if ( ! range ) { this . raiseError ( "Must provide a range for each expected diagnostic, or have one range in the fourslash source." ) ; } return { message : e . message , category , code : e . code , ... ts . createTextSpanFromRange ( range ) , reportsUnnecessary : e . reportsUnnecessary , reportsDeprecated : e . reportsDeprecated } ; } ) ) ; } public verifyQuickInfoAt ( markerName , expectedText , expectedDocumentation ? ) { if ( typeof markerName === "string" ) this . goToMarker ( markerName ) ; else this . goToRangeStart ( markerName ) ; this . verifyQuickInfoString ( expectedText , expectedDocumentation ) ; } public verifyQuickInfos ( namesAndTexts ) { for ( const name in namesAndTexts ) { if ( ts . hasProperty ( namesAndTexts , name ) ) { const text = namesAndTexts [ name ] ; if ( ts . isArray ( text ) ) { assert ( text . length === 2 ) ; const [ expectedText , expectedDocumentation ] = text ; this . verifyQuickInfoAt ( name , expectedText , expectedDocumentation ) ; } else { this . verifyQuickInfoAt ( name , text ) ; } } } } public verifyQuickInfoString ( expectedText , expectedDocumentation ? ) { if ( expectedDocumentation === "" ) { throw new Error ( "Use 'undefined' instead" ) ; } const actualQuickInfo = this . languageService . getQuickInfoAtPosition ( this . activeFile . fileName , this . currentCaretPosition ) ; const actualQuickInfoText = actualQuickInfo ? ts . displayPartsToString ( actualQuickInfo . displayParts ) : "" ; const actualQuickInfoDocumentation = actualQuickInfo ? ts . displayPartsToString ( actualQuickInfo . documentation ) : "" ; assert . equal ( actualQuickInfoText , expectedText , this . messageAtLastKnownMarker ( "quick info text" ) ) ; assert . equal ( actualQuickInfoDocumentation , expectedDocumentation || "" , this . assertionMessageAtLastKnownMarker ( "quick info doc" ) ) ; } public verifyQuickInfoDisplayParts ( kind , kindModifiers , textSpan , displayParts , documentation , tags ) { const actualQuickInfo = this . languageService . getQuickInfoAtPosition ( this . activeFile . fileName , this . currentCaretPosition ) ! ; assert . equal ( actualQuickInfo . kind , kind , this . messageAtLastKnownMarker ( "QuickInfo kind" ) ) ; assert . equal ( actualQuickInfo . kindModifiers , kindModifiers , this . messageAtLastKnownMarker ( "QuickInfo kindModifiers" ) ) ; assert . equal ( JSON . stringify ( actualQuickInfo . textSpan ) , JSON . stringify ( textSpan ) , this . messageAtLastKnownMarker ( "QuickInfo textSpan" ) ) ; assert . equal ( TestState . getDisplayPartsJson ( actualQuickInfo . displayParts ) , TestState . getDisplayPartsJson ( displayParts ) , this . messageAtLastKnownMarker ( "QuickInfo displayParts" ) ) ; assert . equal ( TestState . getDisplayPartsJson ( actualQuickInfo . documentation ) , TestState . getDisplayPartsJson ( documentation ) , this . messageAtLastKnownMarker ( "QuickInfo documentation" ) ) ; if ( ! actualQuickInfo . tags || ! tags ) { assert . equal ( actualQuickInfo . tags , tags , this . messageAtLastKnownMarker ( "QuickInfo tags" ) ) ; } else { assert . equal ( actualQuickInfo . tags . length , tags . length , this . messageAtLastKnownMarker ( "QuickInfo tags" ) ) ; ts . zipWith ( tags , actualQuickInfo . tags , ( expectedTag , actualTag ) => { assert . equal ( expectedTag . name , actualTag . name ) ; assert . equal ( expectedTag . text , actualTag . text , this . messageAtLastKnownMarker ( "QuickInfo tag " + actualTag . name ) ) ; } ) ; } } public verifyRangesAreRenameLocations ( options ? ) { if ( ts . isArray ( options ) ) { this . verifyRenameLocations ( options , options ) ; } else { const ranges = options && options . ranges || this . getRanges ( ) ; this . verifyRenameLocations ( ranges , { ranges , ... options } ) ; } } public verifyRenameLocations ( startRanges , options ) { interface RangeMarkerData { id ? : string ; contextRangeIndex ? : number ; contextRangeDelta ? : number ; contextRangeId ? : string ; } const { findInStrings = false , findInComments = false , ranges = this . getRanges ( ) , providePrefixAndSuffixTextForRename = true } = ts . isArray ( options ) ? { findInStrings : false , findInComments : false , ranges : options , providePrefixAndSuffixTextForRename : true } : options ; const _startRanges = toArray ( startRanges ) ; assert ( _startRanges . length ) ; for ( const startRange of _startRanges ) { this . goToRangeStart ( startRange ) ; const renameInfo = this . languageService . getRenameInfo ( this . activeFile . fileName , this . currentCaretPosition ) ; if ( ! renameInfo . canRename ) { this . raiseError ( "Expected rename to succeed, but it actually failed." ) ; break ; } const references = this . languageService . findRenameLocations ( this . activeFile . fileName , this . currentCaretPosition , findInStrings , findInComments , providePrefixAndSuffixTextForRename ) ; const sort = ( locations ) => locations && ts . sort ( locations , ( r1 , r2 ) => ts . compareStringsCaseSensitive ( r1 . fileName , r2 . fileName ) || r1 . textSpan . start - r2 . textSpan . start ) ; assert . deepEqual ( sort ( references ) , sort ( ranges . map ( ( rangeOrOptions ) : ts . RenameLocation => { const { range , ... prefixSuffixText } = "range" in rangeOrOptions ? rangeOrOptions : { range : rangeOrOptions } ; const { contextRangeIndex , contextRangeDelta , contextRangeId } = ( range . marker && range . marker . data || { } ) as RangeMarkerData ; let contextSpan ; if ( contextRangeDelta !== undefined ) { const allRanges = this . getRanges ( ) ; const index = allRanges . indexOf ( range ) ; if ( index !== - 1 ) { contextSpan = ts . createTextSpanFromRange ( allRanges [ index + contextRangeDelta ] ) ; } } else if ( contextRangeId !== undefined ) { const allRanges = this . getRanges ( ) ; const contextRange = ts . find ( allRanges , range => ( range . marker ?. data as RangeMarkerData ) ?. id === contextRangeId ) ; if ( contextRange ) { contextSpan = ts . createTextSpanFromRange ( contextRange ) ; } } else if ( contextRangeIndex !== undefined ) { contextSpan = ts . createTextSpanFromRange ( this . getRanges ( ) [ contextRangeIndex ] ) ; } return { fileName : range . fileName , textSpan : ts . createTextSpanFromRange ( range ) , ... ( contextSpan ? { contextSpan } : undefined ) , ... prefixSuffixText } ; } ) ) ) ; } } public baselineRename ( marker , options ) { const { fileName , position } = this . getMarkerByName ( marker ) ; const locations = this . languageService . findRenameLocations ( fileName , position , options . findInStrings ?? false , options . findInComments ?? false , options . providePrefixAndSuffixTextForRename ) ; if ( ! locations ) { this . raiseError ( `baselineRename failed. Could not rename at the provided position.` ) ; } const renamesByFile = ts . group ( locations , l => l . fileName ) ; const baselineContent = renamesByFile . map ( renames => { const { fileName } = renames [ 0 ] ; const sortedRenames = ts . sort ( renames , ( a , b ) => b . textSpan . start - a . textSpan . start ) ; let baselineFileContent = this . getFileContent ( fileName ) ; for ( const { textSpan } of sortedRenames ) { const isOriginalSpan = fileName === this . activeFile . fileName && ts . textSpanIntersectsWithPosition ( textSpan , position ) ; baselineFileContent = baselineFileContent . slice ( 0 , textSpan . start ) + ( isOriginalSpan ? "[|RENAME|]" : "RENAME" ) + baselineFileContent . slice ( textSpan . start + textSpan . length ) ; } return `/*====== ${ fileName } ======*/\n\n${ baselineFileContent }` ; } ) . join ( "\n\n" ) + "\n" ; Harness . Baseline . runBaseline ( this . getBaselineFileNameForContainingTestFile ( ) , baselineContent ) ; } public verifyQuickInfoExists ( negative ) { const actualQuickInfo = this . languageService . getQuickInfoAtPosition ( this . activeFile . fileName , this . currentCaretPosition ) ; if ( negative ) { if ( actualQuickInfo ) { this . raiseError ( "verifyQuickInfoExists failed. Expected quick info NOT to exist" ) ; } } else { if ( ! actualQuickInfo ) { this . raiseError ( "verifyQuickInfoExists failed. Expected quick info to exist" ) ; } } } public verifySignatureHelpPresence ( expectPresent , triggerReason , markers ) { if ( markers . length ) { for ( const marker of markers ) { this . goToMarker ( marker ) ; this . verifySignatureHelpPresence ( expectPresent , triggerReason , ts . emptyArray ) ; } return ; } const actual = this . getSignatureHelp ( { triggerReason } ) ; if ( expectPresent !== ! ! actual ) { if ( actual ) { this . raiseError ( `Expected no signature help, but got "${ stringify ( actual ) }"` ) ; } else { this . raiseError ( "Expected signature help, but none was returned." ) ; } } } public verifySignatureHelp ( optionses ) { for ( const options of optionses ) { if ( options . marker === undefined ) { this . verifySignatureHelpWorker ( options ) ; } else { for ( const marker of toArray ( options . marker ) ) { this . goToMarker ( marker ) ; this . verifySignatureHelpWorker ( options ) ; } } } } private verifySignatureHelpWorker ( options ) { const help = this . getSignatureHelp ( { triggerReason : options . triggerReason } ) ! ; if ( ! help ) { this . raiseError ( "Could not get a help signature" ) ; } const selectedItem = help . items [ options . overrideSelectedItemIndex ?? help . selectedItemIndex ] ; const currentParameter = selectedItem . parameters [ help . argumentIndex ] as ts . SignatureHelpParameter | undefined ; assert . equal ( help . items . length , options . overloadsCount || 1 , this . assertionMessageAtLastKnownMarker ( "signature help overloads count" ) ) ; assert . equal ( ts . displayPartsToString ( selectedItem . documentation ) , options . docComment || "" , this . assertionMessageAtLastKnownMarker ( "current signature help doc comment" ) ) ; if ( options . text !== undefined ) { assert . equal ( ts . displayPartsToString ( selectedItem . prefixDisplayParts ) + selectedItem . parameters . map ( p => ts . displayPartsToString ( p . displayParts ) ) . join ( ts . displayPartsToString ( selectedItem . separatorDisplayParts ) ) + ts . displayPartsToString ( selectedItem . suffixDisplayParts ) , options . text ) ; } if ( options . parameterName !== undefined ) { assert . equal ( currentParameter ! . name , options . parameterName ) ; } if ( options . parameterSpan !== undefined ) { assert . equal ( ts . displayPartsToString ( currentParameter ! . displayParts ) , options . parameterSpan ) ; } if ( currentParameter ) { assert . equal ( ts . displayPartsToString ( currentParameter . documentation ) , options . parameterDocComment || "" , this . assertionMessageAtLastKnownMarker ( "current parameter Help DocComment" ) ) ; } if ( options . parameterCount !== undefined ) { assert . equal ( selectedItem . parameters . length , options . parameterCount ) ; } if ( options . argumentCount !== undefined ) { assert . equal ( help . argumentCount , options . argumentCount ) ; } assert . equal ( selectedItem . isVariadic , ! ! options . isVariadic ) ; const actualTags = selectedItem . tags ; assert . equal ( actualTags . length , ( options . tags || ts . emptyArray ) . length , this . assertionMessageAtLastKnownMarker ( "signature help tags" ) ) ; ts . zipWith ( ( options . tags || ts . emptyArray ) , actualTags , ( expectedTag , actualTag ) => { assert . equal ( actualTag . name , expectedTag . name ) ; assert . deepEqual ( actualTag . text , expectedTag . text , this . assertionMessageAtLastKnownMarker ( "signature help tag " + actualTag . name ) ) ; } ) ; const allKeys = [ "marker" , "triggerReason" , "overloadsCount" , "docComment" , "text" , "parameterName" , "parameterSpan" , "parameterDocComment" , "parameterCount" , "isVariadic" , "tags" , "argumentCount" , "overrideSelectedItemIndex" ] ; for ( const key in options ) { if ( ! ts . contains ( allKeys , key ) ) { ts . Debug . fail ( "Unexpected key " + key ) ; } } } private validate ( name , expected , actual ) { if ( expected && expected !== actual ) { this . raiseError ( "Expected " + name + " '" + expected + "'.  Got '" + actual + "' instead." ) ; } } public verifyRenameInfoSucceeded ( displayName , fullDisplayName , kind , kindModifiers , fileToRename , expectedRange , renameInfoOptions ) { const renameInfo = this . languageService . getRenameInfo ( this . activeFile . fileName , this . currentCaretPosition , renameInfoOptions || { allowRenameOfImportPath : true } ) ; if ( ! renameInfo . canRename ) { throw this . raiseError ( "Rename did not succeed" ) ; } this . validate ( "displayName" , displayName , renameInfo . displayName ) ; this . validate ( "fullDisplayName" , fullDisplayName , renameInfo . fullDisplayName ) ; this . validate ( "kind" , kind , renameInfo . kind ) ; this . validate ( "kindModifiers" , kindModifiers , renameInfo . kindModifiers ) ; this . validate ( "fileToRename" , fileToRename , renameInfo . fileToRename ) ; if ( ! expectedRange ) { if ( this . getRanges ( ) . length !== 1 ) { this . raiseError ( "Expected a single range to be selected in the test file." ) ; } expectedRange = this . getRanges ( ) [ 0 ] ; } if ( renameInfo . triggerSpan . start !== expectedRange . pos || ts . textSpanEnd ( renameInfo . triggerSpan ) !== expectedRange . end ) { this . raiseError ( "Expected triggerSpan [" + expectedRange . pos + "," + expectedRange . end + ").  Got [" + renameInfo . triggerSpan . start + "," + ts . textSpanEnd ( renameInfo . triggerSpan ) + ") instead." ) ; } } public verifyRenameInfoFailed ( message ? , allowRenameOfImportPath ? ) { allowRenameOfImportPath = allowRenameOfImportPath === undefined ? true : allowRenameOfImportPath ; const renameInfo = this . languageService . getRenameInfo ( this . activeFile . fileName , this . currentCaretPosition , { allowRenameOfImportPath } ) ; if ( renameInfo . canRename ) { throw this . raiseError ( "Rename was expected to fail" ) ; } this . validate ( "error" , message , renameInfo . localizedErrorMessage ) ; } private alignmentForExtraInfo = 50 ; private spanLines ( file , spanInfo , { selection = false , fullLines = false , lineNumbers = false } = { } ) { if ( selection ) { fullLines = true ; } let contextStartPos = spanInfo . start ; let contextEndPos = contextStartPos + spanInfo . length ; if ( fullLines ) { if ( contextStartPos > 0 ) { while ( contextStartPos > 1 ) { const ch = file . content . charCodeAt ( contextStartPos - 1 ) ; if ( ch === ts . CharacterCodes . lineFeed || ch === ts . CharacterCodes . carriageReturn ) { break ; } contextStartPos -- ; } } if ( contextEndPos < file . content . length ) { while ( contextEndPos < file . content . length - 1 ) { const ch = file . content . charCodeAt ( contextEndPos ) ; if ( ch === ts . CharacterCodes . lineFeed || ch === ts . CharacterCodes . carriageReturn ) { break ; } contextEndPos ++ ; } } } let contextString ; let contextLineMap ; let contextStart ; let contextEnd ; let selectionStart ; let selectionEnd ; let lineNumberPrefixLength ; if ( lineNumbers ) { contextString = file . content ; contextLineMap = ts . computeLineStarts ( contextString ) ; contextStart = ts . computeLineAndCharacterOfPosition ( contextLineMap , contextStartPos ) ; contextEnd = ts . computeLineAndCharacterOfPosition ( contextLineMap , contextEndPos ) ; selectionStart = ts . computeLineAndCharacterOfPosition ( contextLineMap , spanInfo . start ) ; selectionEnd = ts . computeLineAndCharacterOfPosition ( contextLineMap , ts . textSpanEnd ( spanInfo ) ) ; lineNumberPrefixLength = ( contextEnd . line + 1 ) . toString ( ) . length + 2 ; } else { contextString = file . content . substring ( contextStartPos , contextEndPos ) ; contextLineMap = ts . computeLineStarts ( contextString ) ; contextStart = { line : 0 , character : 0 } ; contextEnd = { line : contextLineMap . length - 1 , character : 0 } ; selectionStart = selection ? ts . computeLineAndCharacterOfPosition ( contextLineMap , spanInfo . start - contextStartPos ) : contextStart ; selectionEnd = selection ? ts . computeLineAndCharacterOfPosition ( contextLineMap , ts . textSpanEnd ( spanInfo ) - contextStartPos ) : contextEnd ; lineNumberPrefixLength = 0 ; } const output = [ ] ; for ( let lineNumber = contextStart . line ; lineNumber <= contextEnd . line ; lineNumber ++ ) { const spanLine = contextString . substring ( contextLineMap [ lineNumber ] , contextLineMap [ lineNumber + 1 ] ) ; output . push ( lineNumbers ? `${ ts . padLeft ( `${ lineNumber + 1 }: ` , lineNumberPrefixLength ) }${ spanLine }` : spanLine ) ; if ( selection ) { if ( lineNumber < selectionStart . line || lineNumber > selectionEnd . line ) { continue ; } const isEmpty = selectionStart . line === selectionEnd . line && selectionStart . character === selectionEnd . character ; const selectionPadLength = lineNumber === selectionStart . line ? selectionStart . character : 0 ; const selectionPad = " " . repeat ( selectionPadLength + lineNumberPrefixLength ) ; const selectionLength = isEmpty ? 0 : Math . max ( lineNumber < selectionEnd . line ? spanLine . trimRight ( ) . length - selectionPadLength : selectionEnd . character - selectionPadLength , 1 ) ; const selectionLine = isEmpty ? "<" : "^" . repeat ( selectionLength ) ; output . push ( `${ selectionPad }${ selectionLine }` ) ; } } return output ; } private spanInfoToString ( spanInfo , prefixString , file = this . activeFile ) { let resultString = "SpanInfo: " + JSON . stringify ( spanInfo ) ; if ( spanInfo ) { const spanLines = this . spanLines ( file , spanInfo ) ; for ( let i = 0 ; i < spanLines . length ; i ++ ) { if ( ! i ) { resultString += "\n" ; } resultString += prefixString + spanLines [ i ] ; } resultString += "\n" + prefixString + ":=> (" + this . getLineColStringAtPosition ( spanInfo . start , file ) + ") to (" + this . getLineColStringAtPosition ( ts . textSpanEnd ( spanInfo ) , file ) + ")" ; } return resultString ; } private baselineCurrentFileLocations ( getSpanAtPos ) { const fileLineMap = ts . computeLineStarts ( this . activeFile . content ) ; let nextLine = 0 ; let resultString = "" ; let currentLine ; let previousSpanInfo ; let startColumn ; let length ; const prefixString = "    >" ; let pos = 0 ; const addSpanInfoString = ( ) => { if ( previousSpanInfo ) { resultString += currentLine ; let thisLineMarker = ts . repeatString ( " " , startColumn ! ) + ts . repeatString ( "~" , length ! ) ; thisLineMarker += ts . repeatString ( " " , this . alignmentForExtraInfo - thisLineMarker . length - prefixString . length + 1 ) ; resultString += thisLineMarker ; resultString += "=> Pos: (" + ( pos - length ! ) + " to " + ( pos - 1 ) + ") " ; resultString += " " + previousSpanInfo ; previousSpanInfo = undefined ; } } ; for ( ; pos < this . activeFile . content . length ; pos ++ ) { if ( pos === 0 || pos === fileLineMap [ nextLine ] ) { nextLine ++ ; addSpanInfoString ( ) ; if ( resultString . length ) { resultString += "\n--------------------------------" ; } currentLine = "\n" + nextLine . toString ( ) + ts . repeatString ( " " , 3 - nextLine . toString ( ) . length ) + ">" + this . activeFile . content . substring ( pos , fileLineMap [ nextLine ] ) + "\n    " ; startColumn = 0 ; length = 0 ; } const spanInfo = this . spanInfoToString ( getSpanAtPos ( pos ) , prefixString ) ; if ( previousSpanInfo && previousSpanInfo !== spanInfo ) { addSpanInfoString ( ) ; previousSpanInfo = spanInfo ; startColumn = startColumn ! + length ! ; length = 1 ; } else { previousSpanInfo = spanInfo ; length ! ++ ; } } addSpanInfoString ( ) ; return resultString ; } public getBreakpointStatementLocation ( pos ) { return this . languageService . getBreakpointStatementAtPosition ( this . activeFile . fileName , pos ) ; } public baselineCurrentFileBreakpointLocations ( ) { const baselineFile = this . getBaselineFileNameForInternalFourslashFile ( ) . replace ( "breakpointValidation" , "bpSpan" ) ; Harness . Baseline . runBaseline ( baselineFile , this . baselineCurrentFileLocations ( pos => this . getBreakpointStatementLocation ( pos ) ! ) ) ; } private getEmitFiles ( ) { const emitFiles = [ ] ; const allFourSlashFiles = this . testData . files ; for ( const file of allFourSlashFiles ) { if ( file . fileOptions [ MetadataOptionNames . emitThisFile ] === "true" ) { emitFiles . push ( file ) ; } } if ( emitFiles . length === 0 ) { this . raiseError ( "No emitThisFile is specified in the test file" ) ; } return emitFiles ; } public verifyGetEmitOutput ( expectedOutputFiles ) { const outputFiles = ts . flatMap ( this . getEmitFiles ( ) , e => this . languageService . getEmitOutput ( e . fileName ) . outputFiles ) ; assert . deepEqual ( outputFiles . map ( f => f . name ) , expectedOutputFiles ) ; for ( const { name , text } of outputFiles ) { const fromTestFile = this . getFileContent ( name ) ; if ( fromTestFile !== text ) { this . raiseError ( `Emit output for ${ name } is not as expected: ${ showTextDiff ( fromTestFile , text ) }` ) ; } } } public baselineGetEmitOutput ( ) { let resultString = "" ; for ( const emitFile of this . getEmitFiles ( ) ) { const emitOutput = this . languageService . getEmitOutput ( emitFile . fileName ) ; resultString += "EmitSkipped: " + emitOutput . emitSkipped + Harness . IO . newLine ( ) ; if ( emitOutput . emitSkipped ) { resultString += "Diagnostics:" + Harness . IO . newLine ( ) ; const diagnostics = ts . getPreEmitDiagnostics ( this . languageService . getProgram ( ) ! ) ; for ( const diagnostic of diagnostics ) { if ( ! ts . isString ( diagnostic . messageText ) ) { resultString += this . flattenChainedMessage ( diagnostic . messageText ) ; } else { resultString += "  " + diagnostic . messageText + Harness . IO . newLine ( ) ; } } } for ( const outputFile of emitOutput . outputFiles ) { const fileName = "FileName : " + outputFile . name + Harness . IO . newLine ( ) ; resultString = resultString + Harness . IO . newLine ( ) + fileName + outputFile . text ; } resultString += Harness . IO . newLine ( ) ; } Harness . Baseline . runBaseline ( ts . Debug . checkDefined ( this . testData . globalOptions [ MetadataOptionNames . baselineFile ] ) , resultString ) ; } private flattenChainedMessage ( diag , indent = " " ) { let result = "" ; result += indent + diag . messageText + Harness . IO . newLine ( ) ; if ( diag . next ) { for ( const kid of diag . next ) { result += this . flattenChainedMessage ( kid , indent + " " ) ; } } return result ; } public baselineSyntacticDiagnostics ( ) { const files = this . getCompilerTestFiles ( ) ; const result = this . getSyntacticDiagnosticBaselineText ( files ) ; Harness . Baseline . runBaseline ( this . getBaselineFileNameForContainingTestFile ( ) , result ) ; } private getCompilerTestFiles ( ) { return ts . map ( this . testData . files , ( { content , fileName } ) => ( { content , unitName : fileName } ) ) ; } public baselineSyntacticAndSemanticDiagnostics ( ) { const files = ts . filter ( this . getCompilerTestFiles ( ) , f => ! ts . endsWith ( f . unitName , ".json" ) ) ; const result = this . getSyntacticDiagnosticBaselineText ( files ) + Harness . IO . newLine ( ) + Harness . IO . newLine ( ) + this . getSemanticDiagnosticBaselineText ( files ) ; Harness . Baseline . runBaseline ( this . getBaselineFileNameForContainingTestFile ( ) , result ) ; } private getSyntacticDiagnosticBaselineText ( files ) { const diagnostics = ts . flatMap ( files , file => this . languageService . getSyntacticDiagnostics ( file . unitName ) ) ; const result = `Syntactic Diagnostics for file '${ this . originalInputFileName }':` + Harness . IO . newLine ( ) + Harness . Compiler . getErrorBaseline ( files , diagnostics , false ) ; return result ; } private getSemanticDiagnosticBaselineText ( files ) { const diagnostics = ts . flatMap ( files , file => this . languageService . getSemanticDiagnostics ( file . unitName ) ) ; const result = `Semantic Diagnostics for file '${ this . originalInputFileName }':` + Harness . IO . newLine ( ) + Harness . Compiler . getErrorBaseline ( files , diagnostics , false ) ; return result ; } public baselineQuickInfo ( ) { const baselineFile = this . getBaselineFileNameForContainingTestFile ( ) ; const result = ts . arrayFrom ( this . testData . markerPositions . entries ( ) , ( [ name , marker ] ) => ( { marker : { ... marker , name } , quickInfo : this . languageService . getQuickInfoAtPosition ( marker . fileName , marker . position ) } ) ) ; Harness . Baseline . runBaseline ( baselineFile , stringify ( result ) ) ; } public baselineSignatureHelp ( ) { const baselineFile = this . getBaselineFileNameForContainingTestFile ( ) ; const result = ts . arrayFrom ( this . testData . markerPositions . entries ( ) , ( [ name , marker ] ) => ( { marker : { ... marker , name } , signatureHelp : this . languageService . getSignatureHelpItems ( marker . fileName , marker . position , undefined ) } ) ) ; Harness . Baseline . runBaseline ( baselineFile , stringify ( result ) ) ; } public baselineCompletions ( preferences ? ) { const baselineFile = this . getBaselineFileNameForContainingTestFile ( ) ; const result = ts . arrayFrom ( this . testData . markerPositions . entries ( ) , ( [ name , marker ] ) => { this . goToMarker ( marker ) ; const completions = this . getCompletionListAtCaret ( preferences ) ; return { marker : { ... marker , name } , completionList : { ... completions , entries : completions ?. entries . map ( entry => ( { ... entry , ... this . getCompletionEntryDetails ( entry . name , entry . source , entry . data , preferences ) } ) ) , } } ; } ) ; Harness . Baseline . runBaseline ( baselineFile , stringify ( result ) ) ; } public baselineSmartSelection ( ) { const n = "\n" ; const baselineFile = this . getBaselineFileNameForContainingTestFile ( ) ; const markers = this . getMarkers ( ) ; const fileContent = this . activeFile . content ; const text = markers . map ( marker => { const baselineContent = [ fileContent . slice ( 0 , marker . position ) + "/**/" + fileContent . slice ( marker . position ) + n ] ; let selectionRange = this . languageService . getSmartSelectionRange ( this . activeFile . fileName , marker . position ) ; while ( selectionRange ) { const { textSpan } = selectionRange ; let masked = Array . from ( fileContent ) . map ( ( char , index ) => { const charCode = char . charCodeAt ( 0 ) ; if ( index >= textSpan . start && index < ts . textSpanEnd ( textSpan ) ) { return char === " " ? "•" : ts . isLineBreak ( charCode ) ? `↲${ n }` : char ; } return ts . isLineBreak ( charCode ) ? char : " " ; } ) . join ( "" ) ; masked = masked . replace ( /^\s*$\r?\n?/gm , "" ) ; const isRealCharacter = ( char ) => char !== "•" && char !== "↲" && ! ts . isWhiteSpaceLike ( char . charCodeAt ( 0 ) ) ; const leadingWidth = Array . from ( masked ) . findIndex ( isRealCharacter ) ; const trailingWidth = ts . findLastIndex ( Array . from ( masked ) , isRealCharacter ) ; masked = masked . slice ( 0 , leadingWidth ) + masked . slice ( leadingWidth , trailingWidth ) . replace ( /•/g , " " ) . replace ( /↲/g , "" ) + masked . slice ( trailingWidth ) ; baselineContent . push ( masked ) ; selectionRange = selectionRange . parent ; } return baselineContent . join ( fileContent . includes ( "\n" ) ? n + n : n ) ; } ) . join ( n . repeat ( 2 ) + "=" . repeat ( 80 ) + n . repeat ( 2 ) ) ; Harness . Baseline . runBaseline ( baselineFile , text ) ; } public printBreakpointLocation ( pos ) { Harness . IO . log ( "\n**Pos: " + pos + " " + this . spanInfoToString ( this . getBreakpointStatementLocation ( pos ) ! , "  " ) ) ; } public printBreakpointAtCurrentLocation ( ) { this . printBreakpointLocation ( this . currentCaretPosition ) ; } public printCurrentParameterHelp ( ) { const help = this . languageService . getSignatureHelpItems ( this . activeFile . fileName , this . currentCaretPosition , undefined ) ; Harness . IO . log ( stringify ( help ) ) ; } public printCurrentQuickInfo ( ) { const quickInfo = this . languageService . getQuickInfoAtPosition ( this . activeFile . fileName , this . currentCaretPosition ) ! ; Harness . IO . log ( "Quick Info: " + quickInfo . displayParts ! . map ( part => part . text ) . join ( "" ) ) ; } public printErrorList ( ) { const syntacticErrors = this . languageService . getSyntacticDiagnostics ( this . activeFile . fileName ) ; const semanticErrors = this . languageService . getSemanticDiagnostics ( this . activeFile . fileName ) ; const errorList = ts . concatenate ( syntacticErrors , semanticErrors ) ; Harness . IO . log ( `Error list (${ errorList . length } errors)` ) ; if ( errorList . length ) { errorList . forEach ( err => { Harness . IO . log ( "start: " + err . start + ", length: " + err . length + ", message: " + ts . flattenDiagnosticMessageText ( err . messageText , Harness . IO . newLine ( ) ) ) ; } ) ; } } public printCurrentFileState ( showWhitespace , makeCaretVisible ) { for ( const file of this . testData . files ) { const active = ( this . activeFile === file ) ; Harness . IO . log ( `=== Script (${ file . fileName }) ${ ( active ? "(active, cursor at |)" : "" ) } ===` ) ; let content = this . getFileContent ( file . fileName ) ; if ( active ) { content = content . substr ( 0 , this . currentCaretPosition ) + ( makeCaretVisible ? "|" : "" ) + content . substr ( this . currentCaretPosition ) ; } if ( showWhitespace ) { content = makeWhitespaceVisible ( content ) ; } Harness . IO . log ( content ) ; } } public printCurrentSignatureHelp ( ) { const help = this . getSignatureHelp ( ts . emptyOptions ) ! ; Harness . IO . log ( stringify ( help . items [ help . selectedItemIndex ] ) ) ; } private getBaselineFileNameForInternalFourslashFile ( ext = ".baseline" ) { return this . testData . globalOptions [ MetadataOptionNames . baselineFile ] || ts . getBaseFileName ( this . activeFile . fileName ) . replace ( ts . Extension . Ts , ext ) ; } private getBaselineFileNameForContainingTestFile ( ext = ".baseline" ) { return ts . getBaseFileName ( this . originalInputFileName ) . replace ( ts . Extension . Ts , ext ) ; } private getSignatureHelp ( { triggerReason } ) { return this . languageService . getSignatureHelpItems ( this . activeFile . fileName , this . currentCaretPosition , { triggerReason } ) ; } public printCompletionListMembers ( preferences ) { const completions = this . getCompletionListAtCaret ( preferences ) ; this . printMembersOrCompletions ( completions ) ; } private printMembersOrCompletions ( info ) { if ( info === undefined ) return "No completion info." ; const { entries } = info ; function pad ( s , length ) { return s + new Array ( length - s . length + 1 ) . join ( " " ) ; } function max < T > ( arr , selector ) { return arr . reduce ( ( prev , x ) => Math . max ( prev , selector ( x ) ) , 0 ) ; } const longestNameLength = max ( entries , m => m . name . length ) ; const longestKindLength = max ( entries , m => m . kind . length ) ; entries . sort ( ( m , n ) => m . sortText > n . sortText ? 1 : m . sortText < n . sortText ? - 1 : m . name > n . name ? 1 : m . name < n . name ? - 1 : 0 ) ; const membersString = entries . map ( m => `${ pad ( m . name , longestNameLength ) } ${ pad ( m . kind , longestKindLength ) } ${ m . kindModifiers } ${ m . isRecommended ? "recommended " : "" }${ m . source === undefined ? "" : m . source }` ) . join ( "\n" ) ; Harness . IO . log ( membersString ) ; } public printContext ( ) { ts . forEach ( this . languageServiceAdapterHost . getFilenames ( ) , Harness . IO . log ) ; } public deleteChar ( count = 1 ) { let offset = this . currentCaretPosition ; const ch = "" ; const checkCadence = ( count >> 2 ) + 1 ; for ( let i = 0 ; i < count ; i ++ ) { this . editScriptAndUpdateMarkers ( this . activeFile . fileName , offset , offset + 1 , ch ) ; if ( i % checkCadence === 0 ) { this . checkPostEditInvariants ( ) ; } if ( this . enableFormatting ) { const edits = this . languageService . getFormattingEditsAfterKeystroke ( this . activeFile . fileName , offset , ch , this . formatCodeSettings ) ; if ( edits . length ) { offset += this . applyEdits ( this . activeFile . fileName , edits ) ; } } } this . checkPostEditInvariants ( ) ; } public replace ( start , length , text ) { this . editScriptAndUpdateMarkers ( this . activeFile . fileName , start , start + length , text ) ; this . checkPostEditInvariants ( ) ; } public deleteLineRange ( startIndex , endIndexInclusive ) { const startPos = this . languageServiceAdapterHost . lineAndCharacterToPosition ( this . activeFile . fileName , { line : startIndex , character : 0 } ) ; const endPos = this . languageServiceAdapterHost . lineAndCharacterToPosition ( this . activeFile . fileName , { line : endIndexInclusive + 1 , character : 0 } ) ; this . replace ( startPos , endPos - startPos , "" ) ; } public deleteCharBehindMarker ( count = 1 ) { let offset = this . currentCaretPosition ; const ch = "" ; const checkCadence = ( count >> 2 ) + 1 ; for ( let i = 0 ; i < count ; i ++ ) { this . currentCaretPosition -- ; offset -- ; this . editScriptAndUpdateMarkers ( this . activeFile . fileName , offset , offset + 1 , ch ) ; if ( i % checkCadence === 0 ) { this . checkPostEditInvariants ( ) ; } } this . checkPostEditInvariants ( ) ; } public type ( text , highFidelity = false ) { let offset = this . currentCaretPosition ; const prevChar = " " ; const checkCadence = ( text . length >> 2 ) + 1 ; const selection = this . getSelection ( ) ; this . replace ( selection . pos , selection . end - selection . pos , "" ) ; for ( let i = 0 ; i < text . length ; i ++ ) { const ch = text . charAt ( i ) ; this . editScriptAndUpdateMarkers ( this . activeFile . fileName , offset , offset , ch ) ; if ( highFidelity ) { this . languageService . getBraceMatchingAtPosition ( this . activeFile . fileName , offset ) ; } this . currentCaretPosition ++ ; offset ++ ; if ( highFidelity ) { if ( ch === "(" || ch === "," || ch === "<" ) { this . languageService . getSignatureHelpItems ( this . activeFile . fileName , offset , { triggerReason : { kind : "characterTyped" , triggerCharacter : ch } } ) ; } else if ( prevChar === " " && /A-Za-z_/ . test ( ch ) ) { this . languageService . getCompletionsAtPosition ( this . activeFile . fileName , offset , ts . emptyOptions ) ; } if ( i % checkCadence === 0 ) { this . checkPostEditInvariants ( ) ; } } if ( this . enableFormatting ) { const edits = this . languageService . getFormattingEditsAfterKeystroke ( this . activeFile . fileName , offset , ch , this . formatCodeSettings ) ; if ( edits . length ) { offset += this . applyEdits ( this . activeFile . fileName , edits ) ; } } } this . checkPostEditInvariants ( ) ; } public paste ( text ) { const start = this . currentCaretPosition ; this . editScriptAndUpdateMarkers ( this . activeFile . fileName , this . currentCaretPosition , this . currentCaretPosition , text ) ; this . checkPostEditInvariants ( ) ; const offset = this . currentCaretPosition += text . length ; if ( this . enableFormatting ) { const edits = this . languageService . getFormattingEditsForRange ( this . activeFile . fileName , start , offset , this . formatCodeSettings ) ; if ( edits . length ) { this . applyEdits ( this . activeFile . fileName , edits ) ; } } this . checkPostEditInvariants ( ) ; } private checkPostEditInvariants ( ) { if ( this . testType !== FourSlashTestType . Native ) { return ; } const incrementalSourceFile = this . languageService . getNonBoundSourceFile ( this . activeFile . fileName ) ; Utils . assertInvariants ( incrementalSourceFile , undefined ) ; const incrementalSyntaxDiagnostics = incrementalSourceFile . parseDiagnostics ; const content = this . getFileContent ( this . activeFile . fileName ) ; const referenceSourceFile = ts . createLanguageServiceSourceFile ( this . activeFile . fileName , createScriptSnapShot ( content ) , ts . ScriptTarget . Latest , "0" , false ) ; const referenceSyntaxDiagnostics = referenceSourceFile . parseDiagnostics ; Utils . assertDiagnosticsEquals ( incrementalSyntaxDiagnostics , referenceSyntaxDiagnostics ) ; Utils . assertStructuralEquals ( incrementalSourceFile , referenceSourceFile ) ; } returns private applyEdits ( fileName , edits ) { let runningOffset = 0 ; forEachTextChange ( edits , edit => { const offsetStart = edit . span . start ; const offsetEnd = offsetStart + edit . span . length ; this . editScriptAndUpdateMarkers ( fileName , offsetStart , offsetEnd , edit . newText ) ; const editDelta = edit . newText . length - edit . span . length ; if ( offsetStart <= this . currentCaretPosition ) { if ( offsetEnd <= this . currentCaretPosition ) { this . currentCaretPosition += editDelta ; } else { this . currentCaretPosition = offsetStart ; } } runningOffset += editDelta ; } ) ; return runningOffset ; } public copyFormatOptions ( ) { return ts . clone ( this . formatCodeSettings ) ; } public setFormatOptions ( formatCodeOptions ) { const oldFormatCodeOptions = this . formatCodeSettings ; this . formatCodeSettings = ts . toEditorSettings ( formatCodeOptions ) ; if ( this . testType === FourSlashTestType . Server ) { ( this . languageService as ts . server . SessionClient ) . setFormattingOptions ( this . formatCodeSettings ) ; } return oldFormatCodeOptions ; } public formatDocument ( ) { const edits = this . languageService . getFormattingEditsForDocument ( this . activeFile . fileName , this . formatCodeSettings ) ; this . applyEdits ( this . activeFile . fileName , edits ) ; } public formatSelection ( start , end ) { const edits = this . languageService . getFormattingEditsForRange ( this . activeFile . fileName , start , end , this . formatCodeSettings ) ; this . applyEdits ( this . activeFile . fileName , edits ) ; } public formatOnType ( pos , key ) { const edits = this . languageService . getFormattingEditsAfterKeystroke ( this . activeFile . fileName , pos , key , this . formatCodeSettings ) ; this . applyEdits ( this . activeFile . fileName , edits ) ; } private editScriptAndUpdateMarkers ( fileName , editStart , editEnd , newText ) { this . languageServiceAdapterHost . editScript ( fileName , editStart , editEnd , newText ) ; if ( this . assertTextConsistent ) { this . assertTextConsistent ( fileName ) ; } for ( const marker of this . testData . markers ) { if ( marker . fileName === fileName ) { marker . position = updatePosition ( marker . position , editStart , editEnd , newText ) ; } } for ( const range of this . testData . ranges ) { if ( range . fileName === fileName ) { range . pos = updatePosition ( range . pos , editStart , editEnd , newText ) ; range . end = updatePosition ( range . end , editStart , editEnd , newText ) ; } } this . testData . rangesByText = undefined ; } private removeWhitespace ( text ) { return text . replace ( /\s/g , "" ) ; } public goToBOF ( ) { this . goToPosition ( 0 ) ; } public goToEOF ( ) { const len = this . getFileContent ( this . activeFile . fileName ) . length ; this . goToPosition ( len ) ; } private goToMarkerOrRange ( markerOrRange ) { if ( typeof markerOrRange === "string" ) { this . goToMarker ( markerOrRange ) ; } else { this . goToRangeStart ( markerOrRange ) ; } } public goToRangeStart ( { fileName , pos } ) { this . openFile ( fileName ) ; this . goToPosition ( pos ) ; } public goToTypeDefinition ( definitionIndex ) { const definitions = this . languageService . getTypeDefinitionAtPosition ( this . activeFile . fileName , this . currentCaretPosition ) ! ; if ( ! definitions || ! definitions . length ) { this . raiseError ( "goToTypeDefinition failed - expected to find at least one definition location but got 0" ) ; } if ( definitionIndex >= definitions . length ) { this . raiseError ( `goToTypeDefinition failed - definitionIndex value (${ definitionIndex }) exceeds definition list size (${ definitions . length })` ) ; } const definition = definitions [ definitionIndex ] ; this . openFile ( definition . fileName ) ; this . currentCaretPosition = definition . textSpan . start ; } public verifyTypeDefinitionsCount ( negative , expectedCount ) { const assertFn = negative ? assert . notEqual : assert . equal ; const definitions = this . languageService . getTypeDefinitionAtPosition ( this . activeFile . fileName , this . currentCaretPosition ) ; const actualCount = definitions && definitions . length || 0 ; assertFn ( actualCount , expectedCount , this . messageAtLastKnownMarker ( "Type definitions Count" ) ) ; } public verifyImplementationListIsEmpty ( negative ) { const implementations = this . languageService . getImplementationAtPosition ( this . activeFile . fileName , this . currentCaretPosition ) ; if ( negative ) { assert . isTrue ( implementations && implementations . length > 0 , "Expected at least one implementation but got 0" ) ; } else { assert . isUndefined ( implementations , "Expected implementation list to be empty but implementations returned" ) ; } } public verifyGoToDefinitionName ( expectedName , expectedContainerName ) { const definitions = this . languageService . getDefinitionAtPosition ( this . activeFile . fileName , this . currentCaretPosition ) ; const actualDefinitionName = definitions && definitions . length ? definitions [ 0 ] . name : "" ; const actualDefinitionContainerName = definitions && definitions . length ? definitions [ 0 ] . containerName : "" ; assert . equal ( actualDefinitionName , expectedName , this . messageAtLastKnownMarker ( "Definition Info Name" ) ) ; assert . equal ( actualDefinitionContainerName , expectedContainerName , this . messageAtLastKnownMarker ( "Definition Info Container Name" ) ) ; } public goToImplementation ( ) { const implementations = this . languageService . getImplementationAtPosition ( this . activeFile . fileName , this . currentCaretPosition ) ! ; if ( ! implementations || ! implementations . length ) { this . raiseError ( "goToImplementation failed - expected to find at least one implementation location but got 0" ) ; } if ( implementations . length > 1 ) { this . raiseError ( `goToImplementation failed - more than 1 implementation returned (${ implementations . length })` ) ; } const implementation = implementations [ 0 ] ; this . openFile ( implementation . fileName ) ; this . currentCaretPosition = implementation . textSpan . start ; } public verifyRangesInImplementationList ( markerName ) { this . goToMarker ( markerName ) ; const implementations = this . languageService . getImplementationAtPosition ( this . activeFile . fileName , this . currentCaretPosition ) ! ; if ( ! implementations || ! implementations . length ) { this . raiseError ( "verifyRangesInImplementationList failed - expected to find at least one implementation location but got 0" ) ; } const duplicate = findDuplicatedElement ( implementations , ts . documentSpansEqual ) ; if ( duplicate ) { const { textSpan , fileName } = duplicate ; this . raiseError ( `Duplicate implementations returned for range (${ textSpan . start }, ${ ts . textSpanEnd ( textSpan ) }) in ${ fileName }` ) ; } const ranges = this . getRanges ( ) ; if ( ! ranges || ! ranges . length ) { this . raiseError ( "verifyRangesInImplementationList failed - expected to find at least one range in test source" ) ; } const unsatisfiedRanges = [ ] ; const delayedErrors = [ ] ; for ( const range of ranges ) { const length = range . end - range . pos ; const matchingImpl = ts . find ( implementations , impl => range . fileName === impl . fileName && range . pos === impl . textSpan . start && length === impl . textSpan . length ) ; if ( matchingImpl ) { if ( range . marker && range . marker . data ) { const expected = range . marker . data as { displayParts ? : ts . SymbolDisplayPart [ ] ; parts : string [ ] ; kind ? : string ; } ; if ( expected . displayParts ) { if ( ! ts . arrayIsEqualTo ( expected . displayParts , matchingImpl . displayParts , displayPartIsEqualTo ) ) { delayedErrors . push ( `Mismatched display parts: expected ${ JSON . stringify ( expected . displayParts ) }, actual ${ JSON . stringify ( matchingImpl . displayParts ) }` ) ; } } else if ( expected . parts ) { const actualParts = matchingImpl . displayParts . map ( p => p . text ) ; if ( ! ts . arrayIsEqualTo ( expected . parts , actualParts ) ) { delayedErrors . push ( `Mismatched non-tagged display parts: expected ${ JSON . stringify ( expected . parts ) }, actual ${ JSON . stringify ( actualParts ) }` ) ; } } if ( expected . kind !== undefined ) { if ( expected . kind !== matchingImpl . kind ) { delayedErrors . push ( `Mismatched kind: expected ${ JSON . stringify ( expected . kind ) }, actual ${ JSON . stringify ( matchingImpl . kind ) }` ) ; } } } matchingImpl . matched = true ; } else { unsatisfiedRanges . push ( range ) ; } } if ( delayedErrors . length ) { this . raiseError ( delayedErrors . join ( "\n" ) ) ; } const unmatchedImplementations = implementations . filter ( impl => ! impl . matched ) ; if ( unmatchedImplementations . length || unsatisfiedRanges . length ) { let error = "Not all ranges or implementations are satisfied" ; if ( unsatisfiedRanges . length ) { error += "\nUnsatisfied ranges:" ; for ( const range of unsatisfiedRanges ) { error += `\n    (${ range . pos }, ${ range . end }) in ${ range . fileName }: ${ this . rangeText ( range ) }` ; } } if ( unmatchedImplementations . length ) { error += "\nUnmatched implementations:" ; for ( const impl of unmatchedImplementations ) { const end = impl . textSpan . start + impl . textSpan . length ; error += `\n    (${ impl . textSpan . start }, ${ end }) in ${ impl . fileName }: ${ this . getFileContent ( impl . fileName ) . slice ( impl . textSpan . start , end ) }` ; } } this . raiseError ( error ) ; } function displayPartIsEqualTo ( a , b ) { return a . kind === b . kind && a . text === b . text ; } } public getMarkers ( ) { return this . testData . markers . slice ( 0 ) ; } public getMarkerNames ( ) { return ts . arrayFrom ( this . testData . markerPositions . keys ( ) ) ; } public getRanges ( ) { return this . testData . ranges ; } public getRangesInFile ( fileName = this . activeFile . fileName ) { return this . getRanges ( ) . filter ( r => r . fileName === fileName ) ; } public rangesByText ( ) { if ( this . testData . rangesByText ) return this . testData . rangesByText ; const result = ts . createMultiMap < Range > ( ) ; this . testData . rangesByText = result ; for ( const range of this . getRanges ( ) ) { const text = this . rangeText ( range ) ; result . add ( text , range ) ; } return result ; } private rangeText ( { fileName , pos , end } ) { return this . getFileContent ( fileName ) . slice ( pos , end ) ; } public verifyCaretAtMarker ( markerName = "" ) { const pos = this . getMarkerByName ( markerName ) ; if ( pos . fileName !== this . activeFile . fileName ) { throw new Error ( `verifyCaretAtMarker failed - expected to be in file "${ pos . fileName }", but was in file "${ this . activeFile . fileName }"` ) ; } if ( pos . position !== this . currentCaretPosition ) { throw new Error ( `verifyCaretAtMarker failed - expected to be at marker "/*${ markerName }*/, but was at position ${ this . currentCaretPosition }(${ this . getLineColStringAtPosition ( this . currentCaretPosition ) })` ) ; } } private getIndentation ( fileName , position , indentStyle , baseIndentSize ) { const formatOptions = ts . clone ( this . formatCodeSettings ) ; formatOptions . indentStyle = indentStyle ; formatOptions . baseIndentSize = baseIndentSize ; return this . languageService . getIndentationAtPosition ( fileName , position , formatOptions ) ; } public verifyIndentationAtCurrentPosition ( numberOfSpaces , indentStyle = ts . IndentStyle . Smart , baseIndentSize = 0 ) { const actual = this . getIndentation ( this . activeFile . fileName , this . currentCaretPosition , indentStyle , baseIndentSize ) ; const lineCol = this . getLineColStringAtPosition ( this . currentCaretPosition ) ; if ( actual !== numberOfSpaces ) { this . raiseError ( `verifyIndentationAtCurrentPosition failed at ${ lineCol } - expected: ${ numberOfSpaces }, actual: ${ actual }` ) ; } } public verifyIndentationAtPosition ( fileName , position , numberOfSpaces , indentStyle = ts . IndentStyle . Smart , baseIndentSize = 0 ) { const actual = this . getIndentation ( fileName , position , indentStyle , baseIndentSize ) ; const lineCol = this . getLineColStringAtPosition ( position ) ; if ( actual !== numberOfSpaces ) { this . raiseError ( `verifyIndentationAtPosition failed at ${ lineCol } - expected: ${ numberOfSpaces }, actual: ${ actual }` ) ; } } public verifyCurrentLineContent ( text ) { const actual = this . getCurrentLineContent ( ) ; if ( actual !== text ) { throw new Error ( "verifyCurrentLineContent\n" + displayExpectedAndActualString ( text , actual , true ) ) ; } } public verifyCurrentFileContent ( text ) { this . verifyFileContent ( this . activeFile . fileName , text ) ; } private verifyFileContent ( fileName , text ) { const actual = this . getFileContent ( fileName ) ; if ( actual !== text ) { throw new Error ( `verifyFileContent failed:\n${ showTextDiff ( text , actual ) }` ) ; } } public verifyFormatDocumentChangesNothing ( ) { const { fileName } = this . activeFile ; const before = this . getFileContent ( fileName ) ; this . formatDocument ( ) ; this . verifyFileContent ( fileName , before ) ; } public verifyTextAtCaretIs ( text ) { const actual = this . getFileContent ( this . activeFile . fileName ) . substring ( this . currentCaretPosition , this . currentCaretPosition + text . length ) ; if ( actual !== text ) { throw new Error ( "verifyTextAtCaretIs\n" + displayExpectedAndActualString ( text , actual , true ) ) ; } } public verifyCurrentNameOrDottedNameSpanText ( text ) { const span = this . languageService . getNameOrDottedNameSpan ( this . activeFile . fileName , this . currentCaretPosition , this . currentCaretPosition ) ; if ( ! span ) { return this . raiseError ( "verifyCurrentNameOrDottedNameSpanText\n" + displayExpectedAndActualString ( "\"" + text + "\"" , "undefined" ) ) ; } const actual = this . getFileContent ( this . activeFile . fileName ) . substring ( span . start , ts . textSpanEnd ( span ) ) ; if ( actual !== text ) { this . raiseError ( "verifyCurrentNameOrDottedNameSpanText\n" + displayExpectedAndActualString ( text , actual , true ) ) ; } } private getNameOrDottedNameSpan ( pos ) { return this . languageService . getNameOrDottedNameSpan ( this . activeFile . fileName , pos , pos ) ; } public baselineCurrentFileNameOrDottedNameSpans ( ) { Harness . Baseline . runBaseline ( this . testData . globalOptions [ MetadataOptionNames . baselineFile ] , this . baselineCurrentFileLocations ( pos => this . getNameOrDottedNameSpan ( pos ) ! ) ) ; } public printNameOrDottedNameSpans ( pos ) { Harness . IO . log ( this . spanInfoToString ( this . getNameOrDottedNameSpan ( pos ) ! , "**" ) ) ; } private classificationToIdentifier ( classification ) { const tokenTypes = [ ] ; tokenTypes [ ts . classifier . v2020 . TokenType . class ] = "class" ; tokenTypes [ ts . classifier . v2020 . TokenType . enum ] = "enum" ; tokenTypes [ ts . classifier . v2020 . TokenType . interface ] = "interface" ; tokenTypes [ ts . classifier . v2020 . TokenType . namespace ] = "namespace" ; tokenTypes [ ts . classifier . v2020 . TokenType . typeParameter ] = "typeParameter" ; tokenTypes [ ts . classifier . v2020 . TokenType . type ] = "type" ; tokenTypes [ ts . classifier . v2020 . TokenType . parameter ] = "parameter" ; tokenTypes [ ts . classifier . v2020 . TokenType . variable ] = "variable" ; tokenTypes [ ts . classifier . v2020 . TokenType . enumMember ] = "enumMember" ; tokenTypes [ ts . classifier . v2020 . TokenType . property ] = "property" ; tokenTypes [ ts . classifier . v2020 . TokenType . function ] = "function" ; tokenTypes [ ts . classifier . v2020 . TokenType . member ] = "member" ; const tokenModifiers = [ ] ; tokenModifiers [ ts . classifier . v2020 . TokenModifier . async ] = "async" ; tokenModifiers [ ts . classifier . v2020 . TokenModifier . declaration ] = "declaration" ; tokenModifiers [ ts . classifier . v2020 . TokenModifier . readonly ] = "readonly" ; tokenModifiers [ ts . classifier . v2020 . TokenModifier . static ] = "static" ; tokenModifiers [ ts . classifier . v2020 . TokenModifier . local ] = "local" ; tokenModifiers [ ts . classifier . v2020 . TokenModifier . defaultLibrary ] = "defaultLibrary" ; function getTokenTypeFromClassification ( tsClassification ) { if ( tsClassification > ts . classifier . v2020 . TokenEncodingConsts . modifierMask ) { return ( tsClassification >> ts . classifier . v2020 . TokenEncodingConsts . typeOffset ) - 1 ; } return undefined ; } function getTokenModifierFromClassification ( tsClassification ) { return tsClassification & ts . classifier . v2020 . TokenEncodingConsts . modifierMask ; } const typeIdx = getTokenTypeFromClassification ( classification ) || 0 ; const modSet = getTokenModifierFromClassification ( classification ) ; return [ tokenTypes [ typeIdx ] , ... tokenModifiers . filter ( ( _ , i ) => modSet & 1 << i ) ] . join ( "." ) ; } private verifyClassifications ( expected , actual , sourceFileText ) { if ( actual . length !== expected . length ) { this . raiseError ( "verifyClassifications failed - expected total classifications to be " + expected . length + ", but was " + actual . length + jsonMismatchString ( ) ) ; } ts . zipWith ( expected , actual , ( expectedClassification , actualClassification ) => { const expectedType = expectedClassification . classificationType ; const actualType = typeof actualClassification . classificationType === "number" ? this . classificationToIdentifier ( actualClassification . classificationType ) : actualClassification . classificationType ; if ( expectedType !== actualType ) { this . raiseError ( "verifyClassifications failed - expected classifications type to be " + expectedType + ", but was " + actualType + jsonMismatchString ( ) ) ; } const expectedSpan = expectedClassification . textSpan ; const actualSpan = actualClassification . textSpan ; if ( expectedSpan ) { const expectedLength = expectedSpan . end - expectedSpan . start ; if ( expectedSpan . start !== actualSpan . start || expectedLength !== actualSpan . length ) { this . raiseError ( "verifyClassifications failed - expected span of text to be " + "{start=" + expectedSpan . start + ", length=" + expectedLength + "}, but was " + "{start=" + actualSpan . start + ", length=" + actualSpan . length + "}" + jsonMismatchString ( ) ) ; } } const actualText = this . activeFile . content . substr ( actualSpan . start , actualSpan . length ) ; if ( expectedClassification . text !== actualText ) { this . raiseError ( "verifyClassifications failed - expected classified text to be " + expectedClassification . text + ", but was " + actualText + jsonMismatchString ( ) ) ; } } ) ; function jsonMismatchString ( ) { const showActual = actual . map ( ( { classificationType , textSpan } ) => ( { classificationType , text : sourceFileText . slice ( textSpan . start , textSpan . start + textSpan . length ) } ) ) ; return Harness . IO . newLine ( ) + "expected: '" + Harness . IO . newLine ( ) + stringify ( expected ) + "'" + Harness . IO . newLine ( ) + "actual:   '" + Harness . IO . newLine ( ) + stringify ( showActual ) + "'" ; } } public verifyProjectInfo ( expected ) { if ( this . testType === FourSlashTestType . Server ) { const actual = ( this . languageService as ts . server . SessionClient ) . getProjectInfo ( this . activeFile . fileName , true ) ; assert . equal ( expected . join ( "," ) , actual . fileNames ! . map ( file => { return file . replace ( this . basePath + "/" , "" ) ; } ) . join ( "," ) ) ; } } public replaceWithSemanticClassifications ( format ) { const actual = this . languageService . getSemanticClassifications ( this . activeFile . fileName , ts . createTextSpan ( 0 , this . activeFile . content . length ) , format ) ; const replacement = [ `const c2 = classification("2020");` , `verify.semanticClassificationsAre("2020",` ] ; for ( const a of actual ) { const identifier = this . classificationToIdentifier ( a . classificationType as number ) ; const text = this . activeFile . content . slice ( a . textSpan . start , a . textSpan . start + a . textSpan . length ) ; replacement . push ( `    c2.semanticToken("${ identifier }", "${ text }"), ` ) ; } ; replacement . push ( ");" ) ; throw new Error ( "You need to change the source code of fourslash test to use replaceWithSemanticClassifications" ) ; } public verifyEncodedSemanticClassificationsLength ( format , expected ) { const actual = this . languageService . getEncodedSemanticClassifications ( this . activeFile . fileName , ts . createTextSpan ( 0 , this . activeFile . content . length ) , format ) ; if ( actual . spans . length !== expected ) { this . raiseError ( `encodedSemanticClassificationsLength failed - expected total spans to be ${ expected } got ${ actual . spans . length }` ) ; } } public verifySemanticClassifications ( format , expected ) { const actual = this . languageService . getSemanticClassifications ( this . activeFile . fileName , ts . createTextSpan ( 0 , this . activeFile . content . length ) , format ) ; this . verifyClassifications ( expected , actual , this . activeFile . content ) ; } public verifySyntacticClassifications ( expected ) { const actual = this . languageService . getSyntacticClassifications ( this . activeFile . fileName , ts . createTextSpan ( 0 , this . activeFile . content . length ) ) ; this . verifyClassifications ( expected , actual , this . activeFile . content ) ; } public printOutliningSpans ( ) { const spans = this . languageService . getOutliningSpans ( this . activeFile . fileName ) ; Harness . IO . log ( `Outlining spans (${ spans . length } items)\nResults:` ) ; Harness . IO . log ( stringify ( spans ) ) ; this . printOutliningSpansInline ( spans ) ; } private printOutliningSpansInline ( spans ) { const allSpanInsets = [ ] as { text : string ; pos : number ; } [ ] ; let annotated = this . activeFile . content ; ts . forEach ( spans , span => { allSpanInsets . push ( { text : "[|" , pos : span . textSpan . start } ) ; allSpanInsets . push ( { text : "|]" , pos : span . textSpan . start + span . textSpan . length } ) ; } ) ; const reverseSpans = allSpanInsets . sort ( ( l , r ) => r . pos - l . pos ) ; ts . forEach ( reverseSpans , span => { annotated = annotated . slice ( 0 , span . pos ) + span . text + annotated . slice ( span . pos ) ; } ) ; Harness . IO . log ( `\nMockup:\n${ annotated }` ) ; } public verifyOutliningSpans ( spans , kind ? ) { const actual = this . languageService . getOutliningSpans ( this . activeFile . fileName ) ; const filterActual = ts . filter ( actual , f => kind === undefined ? true : f . kind === kind ) ; if ( filterActual . length !== spans . length ) { this . raiseError ( `verifyOutliningSpans failed - expected total spans to be ${ spans . length }, but was ${ actual . length }\n\nFound Spans:\n\n${ this . printOutliningSpansInline ( actual ) }` ) ; } ts . zipWith ( spans , filterActual , ( expectedSpan , actualSpan , i ) => { if ( expectedSpan . pos !== actualSpan . textSpan . start || expectedSpan . end !== ts . textSpanEnd ( actualSpan . textSpan ) ) { return this . raiseError ( `verifyOutliningSpans failed - span ${ ( i + 1 ) } expected: (${ expectedSpan . pos },${ expectedSpan . end }),  actual: (${ actualSpan . textSpan . start },${ ts . textSpanEnd ( actualSpan . textSpan ) })` ) ; } if ( kind !== undefined && actualSpan . kind !== kind ) { return this . raiseError ( `verifyOutliningSpans failed - span ${ ( i + 1 ) } expected kind: ('${ kind }'),  actual: ('${ actualSpan . kind }')` ) ; } } ) ; } public verifyOutliningHintSpans ( spans ) { const actual = this . languageService . getOutliningSpans ( this . activeFile . fileName ) ; if ( actual . length !== spans . length ) { this . raiseError ( `verifyOutliningHintSpans failed - expected total spans to be ${ spans . length }, but was ${ actual . length }` ) ; } ts . zipWith ( spans , actual , ( expectedSpan , actualSpan , i ) => { if ( expectedSpan . pos !== actualSpan . hintSpan . start || expectedSpan . end !== ts . textSpanEnd ( actualSpan . hintSpan ) ) { return this . raiseError ( `verifyOutliningSpans failed - span ${ ( i + 1 ) } expected: (${ expectedSpan . pos },${ expectedSpan . end }),  actual: (${ actualSpan . hintSpan . start },${ ts . textSpanEnd ( actualSpan . hintSpan ) })` ) ; } } ) ; } public verifyTodoComments ( descriptors , spans ) { const actual = this . languageService . getTodoComments ( this . activeFile . fileName , descriptors . map ( d => ( { text : d , priority : 0 } ) ) ) ; if ( actual . length !== spans . length ) { this . raiseError ( `verifyTodoComments failed - expected total spans to be ${ spans . length }, but was ${ actual . length }` ) ; } ts . zipWith ( spans , actual , ( expectedSpan , actualComment , i ) => { const actualCommentSpan = ts . createTextSpan ( actualComment . position , actualComment . message . length ) ; if ( expectedSpan . pos !== actualCommentSpan . start || expectedSpan . end !== ts . textSpanEnd ( actualCommentSpan ) ) { this . raiseError ( `verifyOutliningSpans failed - span ${ ( i + 1 ) } expected: (${ expectedSpan . pos },${ expectedSpan . end }),  actual: (${ actualCommentSpan . start },${ ts . textSpanEnd ( actualCommentSpan ) })` ) ; } } ) ; } param errorCode param index public getAndApplyCodeActions ( errorCode ? , index ? ) { const fileName = this . activeFile . fileName ; const fixes = this . getCodeFixes ( fileName , errorCode ) ; if ( index === undefined ) { if ( ! ( fixes && fixes . length === 1 ) ) { this . raiseError ( `Should find exactly one codefix, but ${ fixes ? fixes . length : "none" } found. ${ fixes ? fixes . map ( a => `${ Harness . IO . newLine ( ) } "${ a . description }"` ) : "" }` ) ; } index = 0 ; } else { if ( ! ( fixes && fixes . length >= index + 1 ) ) { this . raiseError ( `Should find at least ${ index + 1 } codefix(es), but ${ fixes ? fixes . length : "none" } found.` ) ; } } this . applyChanges ( fixes [ index ] . changes ) ; } public applyCodeActionFromCompletion ( markerName , options ) { this . goToMarker ( markerName ) ; const details = this . getCompletionEntryDetails ( options . name , options . source , options . data , options . preferences ) ; if ( ! details ) { const completions = this . getCompletionListAtCaret ( options . preferences ) ?. entries ; const matchingName = completions ?. filter ( e => e . name === options . name ) ; const detailMessage = matchingName ?. length ? `\n  Found ${ matchingName . length } with name '${ options . name }' from source(s) ${ matchingName . map ( e => `'${ e . source }'` ) . join ( ", " ) }.` : ` (In fact, there were no completions with name '${ options . name }' at all.)` ; return this . raiseError ( `No completions were found for the given name, source/data, and preferences.` + detailMessage ) ; } const codeActions = details . codeActions ; if ( codeActions ?. length !== 1 ) { this . raiseError ( `Expected one code action, got ${ codeActions ?. length ?? 0 }` ) ; } const codeAction = ts . first ( codeActions ) ; if ( codeAction . description !== options . description ) { this . raiseError ( `Expected description to be:\n${ options . description }\ngot:\n${ codeActions [ 0 ] . description }` ) ; } this . applyChanges ( codeAction . changes ) ; this . verifyNewContentAfterChange ( options , ts . flatMap ( codeActions , a => a . changes . map ( c => c . fileName ) ) ) ; } public verifyRangeIs ( expectedText , includeWhiteSpace ? ) { this . verifyTextMatches ( this . rangeText ( this . getOnlyRange ( ) ) , ! ! includeWhiteSpace , expectedText ) ; } private getOnlyRange ( ) { const ranges = this . getRanges ( ) ; if ( ranges . length !== 1 ) { this . raiseError ( "Exactly one range should be specified in the testfile." ) ; } return ts . first ( ranges ) ; } private verifyTextMatches ( actualText , includeWhitespace , expectedText ) { const removeWhitespace = ( s ) : string => includeWhitespace ? s : this . removeWhitespace ( s ) ; if ( removeWhitespace ( actualText ) !== removeWhitespace ( expectedText ) ) { this . raiseError ( `Actual range text doesn't match expected text.\n${ showTextDiff ( expectedText , actualText ) }` ) ; } } /**
         * Compares expected text to the text that would be in the sole range
         * (ie: [|...|]) in the file after applying the codefix sole codefix
         * in the source file.
         */ public verifyRangeAfterCodeFix ( expectedText , includeWhiteSpace ? , errorCode ? , index ? ) { this . getAndApplyCodeActions ( errorCode , index ) ; this . verifyRangeIs ( expectedText , includeWhiteSpace ) ; } public verifyCodeFixAll ( { fixId , fixAllDescription , newFileContent , commands : expectedCommands } ) { const fixWithId = ts . find ( this . getCodeFixes ( this . activeFile . fileName ) , a => a . fixId === fixId ) ; ts . Debug . assert ( fixWithId !== undefined , "No available code fix has the expected id. Fix All is not available if there is only one potentially fixable diagnostic present." , ( ) => `Expected '${ fixId }'. Available actions:\n${ ts . mapDefined ( this . getCodeFixes ( this . activeFile . fileName ) , a => `${ a . fixName } (${ a . fixId || "no fix id" })` ) . join ( "\n" ) }` ) ; ts . Debug . assertEqual ( fixWithId . fixAllDescription , fixAllDescription ) ; const { changes , commands } = this . languageService . getCombinedCodeFix ( { type : "file" , fileName : this . activeFile . fileName } , fixId , this . formatCodeSettings , ts . emptyOptions ) ; assert . deepEqual < readonly { } [ ] | undefined > ( commands , expectedCommands ) ; this . verifyNewContent ( { newFileContent } , changes ) ; } public verifyCodeFix ( options ) { const fileName = this . activeFile . fileName ; const actions = this . getCodeFixes ( fileName , options . errorCode , options . preferences ) ; let index = options . index ; if ( index === undefined ) { if ( ! ( actions && actions . length === 1 ) ) { this . raiseError ( `Should find exactly one codefix, but ${ actions ? actions . length : "none" } found. ${ actions ? actions . map ( a => `${ Harness . IO . newLine ( ) } "${ a . description }"` ) : "" }` ) ; } index = 0 ; } else { if ( ! ( actions && actions . length >= index + 1 ) ) { this . raiseError ( `Should find at least ${ index + 1 } codefix(es), but ${ actions ? actions . length : "none" } found.` ) ; } } const action = actions [ index ] ; if ( typeof options . description === "string" ) { assert . equal ( action . description , options . description ) ; } else if ( Array . isArray ( options . description ) ) { const description = ts . formatStringFromArgs ( options . description [ 0 ] , options . description , 1 ) ; assert . equal ( action . description , description ) ; } else { assert . match ( action . description , templateToRegExp ( options . description . template ) ) ; } assert . deepEqual ( action . commands , options . commands ) ; if ( options . applyChanges ) { for ( const change of action . changes ) { this . applyEdits ( change . fileName , change . textChanges ) ; } this . verifyNewContentAfterChange ( options , action . changes . map ( c => c . fileName ) ) ; } else { this . verifyNewContent ( options , action . changes ) ; } } private verifyNewContent ( { newFileContent , newRangeContent } , changes ) { if ( newRangeContent !== undefined ) { assert ( newFileContent === undefined ) ; assert ( changes . length === 1 , "Affected 0 or more than 1 file, must use 'newFileContent' instead of 'newRangeContent'" ) ; const change = ts . first ( changes ) ; assert ( change . fileName = this . activeFile . fileName ) ; const newText = ts . textChanges . applyChanges ( this . getFileContent ( this . activeFile . fileName ) , change . textChanges ) ; const newRange = updateTextRangeForTextChanges ( this . getOnlyRange ( ) , change . textChanges ) ; const actualText = newText . slice ( newRange . pos , newRange . end ) ; this . verifyTextMatches ( actualText , true , newRangeContent ) ; } else { if ( newFileContent === undefined ) throw ts . Debug . fail ( ) ; if ( typeof newFileContent !== "object" ) newFileContent = { [ this . activeFile . fileName ] : newFileContent } ; for ( const change of changes ) { const expectedNewContent = newFileContent [ change . fileName ] ; if ( expectedNewContent === undefined ) { ts . Debug . fail ( `Did not expect a change in ${ change . fileName }` ) ; } const oldText = this . tryGetFileContent ( change . fileName ) ; ts . Debug . assert ( ! ! change . isNewFile === ( oldText === undefined ) ) ; const newContent = change . isNewFile ? ts . first ( change . textChanges ) . newText : ts . textChanges . applyChanges ( oldText ! , change . textChanges ) ; this . verifyTextMatches ( newContent , true , expectedNewContent ) ; } for ( const newFileName in newFileContent ) { ts . Debug . assert ( changes . some ( c => c . fileName === newFileName ) , "No change in file" , ( ) => newFileName ) ; } } } private verifyNewContentAfterChange ( { newFileContent , newRangeContent } , changedFiles ) { const assertedChangedFiles = ! newFileContent || typeof newFileContent === "string" ? [ this . activeFile . fileName ] : ts . getOwnKeys ( newFileContent ) ; assert . deepEqual ( assertedChangedFiles , changedFiles ) ; if ( newFileContent !== undefined ) { assert ( ! newRangeContent ) ; if ( typeof newFileContent === "string" ) { this . verifyCurrentFileContent ( newFileContent ) ; } else { for ( const fileName in newFileContent ) { this . verifyFileContent ( fileName , newFileContent [ fileName ] ) ; } } } else { this . verifyRangeIs ( newRangeContent ! , true ) ; } } param fileName private getCodeFixes ( fileName , errorCode ? , preferences = ts . emptyOptions , position ? ) { const diagnosticsForCodeFix = this . getDiagnostics ( fileName , true ) . map ( diagnostic => ( { start : diagnostic . start , length : diagnostic . length , code : diagnostic . code } ) ) ; return ts . flatMap ( ts . deduplicate ( diagnosticsForCodeFix , ts . equalOwnProperties ) , diagnostic => { if ( errorCode !== undefined && errorCode !== diagnostic . code ) { return ; } if ( position !== undefined && diagnostic . start !== undefined && diagnostic . length !== undefined ) { const span = ts . createTextRangeFromSpan ( { start : diagnostic . start , length : diagnostic . length } ) ; if ( ! ts . textRangeContainsPositionInclusive ( span , position ) ) { return ; } } return this . languageService . getCodeFixesAtPosition ( fileName , diagnostic . start ! , diagnostic . start ! + diagnostic . length ! , [ diagnostic . code ] , this . formatCodeSettings , preferences ) ; } ) ; } private applyChanges ( changes ) { for ( const change of changes ) { this . applyEdits ( change . fileName , change . textChanges ) ; } } public verifyImportFixAtPosition ( expectedTextArray , errorCode , preferences ) { const { fileName } = this . activeFile ; const ranges = this . getRanges ( ) . filter ( r => r . fileName === fileName ) ; if ( ranges . length > 1 ) { this . raiseError ( "Exactly one range should be specified in the testfile." ) ; } const range = ts . firstOrUndefined ( ranges ) ; if ( preferences ) { this . configure ( preferences ) ; } const codeFixes = this . getCodeFixes ( fileName , errorCode , preferences ) . filter ( f => f . fixName === ts . codefix . importFixName ) ; if ( codeFixes . length === 0 ) { if ( expectedTextArray . length !== 0 ) { this . raiseError ( "No codefixes returned." ) ; } return ; } const actualTextArray = [ ] ; const scriptInfo = this . languageServiceAdapterHost . getScriptInfo ( fileName ) ! ; const originalContent = scriptInfo . content ; for ( const codeFix of codeFixes ) { ts . Debug . assert ( codeFix . changes . length === 1 ) ; const change = ts . first ( codeFix . changes ) ; ts . Debug . assert ( change . fileName === fileName ) ; this . applyEdits ( change . fileName , change . textChanges ) ; const text = range ? this . rangeText ( range ) : this . getFileContent ( fileName ) ; actualTextArray . push ( text ) ; const span = change . textChanges [ 0 ] . span ; const deletedText = originalContent . substr ( span . start , change . textChanges [ 0 ] . span . length ) ; const insertedText = change . textChanges [ 0 ] . newText ; this . editScriptAndUpdateMarkers ( fileName , span . start , span . start + insertedText . length , deletedText ) ; } if ( expectedTextArray . length !== actualTextArray . length ) { this . raiseError ( `Expected ${ expectedTextArray . length } import fixes, got ${ actualTextArray . length }:\n\n${ actualTextArray . join ( "\n\n" + "-" . repeat ( 20 ) + "\n\n" ) }` ) ; } ts . zipWith ( expectedTextArray , actualTextArray , ( expected , actual , index ) => { if ( expected !== actual ) { this . raiseError ( `Import fix at index ${ index } doesn't match.\n${ showTextDiff ( expected , actual ) }` ) ; } } ) ; } public verifyImportFixModuleSpecifiers ( markerName , moduleSpecifiers ) { const marker = this . getMarkerByName ( markerName ) ; const codeFixes = this . getCodeFixes ( marker . fileName , ts . Diagnostics . Cannot_find_name_0 . code , { includeCompletionsForModuleExports : true , includeCompletionsWithInsertText : true } , marker . position ) . filter ( f => f . fixName === ts . codefix . importFixName ) ; const actualModuleSpecifiers = ts . mapDefined ( codeFixes , fix => { return ts . forEach ( ts . flatMap ( fix . changes , c => c . textChanges ) , c => { const match = /(?:from |require\()(['"])((?:(?!\1).)*)\1/ . exec ( c . newText ) ; return match ?. [ 2 ] ; } ) ; } ) ; assert . deepEqual ( actualModuleSpecifiers , moduleSpecifiers ) ; } public verifyDocCommentTemplate ( expected , options ? ) { const name = "verifyDocCommentTemplate" ; const actual = this . languageService . getDocCommentTemplateAtPosition ( this . activeFile . fileName , this . currentCaretPosition , options || { generateReturnInDocTemplate : true } ) ! ; if ( expected === undefined ) { if ( actual ) { this . raiseError ( `${ name } failed - expected no template but got {newText: "${ actual . newText }", caretOffset: ${ actual . caretOffset }}` ) ; } return ; } else { if ( actual === undefined ) { this . raiseError ( `${ name } failed - expected the template {newText: "${ expected . newText }", caretOffset: "${ expected . caretOffset }"} but got nothing instead` ) ; } if ( actual . newText !== expected . newText ) { this . raiseError ( `${ name } failed for expected insertion.\n${ showTextDiff ( expected . newText , actual . newText ) }` ) ; } if ( actual . caretOffset !== expected . caretOffset ) { this . raiseError ( `${ name } failed - expected caretOffset: ${ expected . caretOffset }\nactual caretOffset:${ actual . caretOffset }` ) ; } } } public verifyBraceCompletionAtPosition ( negative , openingBrace ) { const openBraceMap = new ts . Map ( ts . getEntries < ts . CharacterCodes > ( { "(" : ts . CharacterCodes . openParen , "{" : ts . CharacterCodes . openBrace , "[" : ts . CharacterCodes . openBracket , "'" : ts . CharacterCodes . singleQuote , '"' : ts . CharacterCodes . doubleQuote , "`" : ts . CharacterCodes . backtick , "<" : ts . CharacterCodes . lessThan } ) ) ; const charCode = openBraceMap . get ( openingBrace ) ; if ( ! charCode ) { throw this . raiseError ( `Invalid openingBrace '${ openingBrace }' specified.` ) ; } const position = this . currentCaretPosition ; const validBraceCompletion = this . languageService . isValidBraceCompletionAtPosition ( this . activeFile . fileName , position , charCode ) ; if ( ! negative && ! validBraceCompletion ) { this . raiseError ( `${ position } is not a valid brace completion position for ${ openingBrace }` ) ; } if ( negative && validBraceCompletion ) { this . raiseError ( `${ position } is a valid brace completion position for ${ openingBrace }` ) ; } } public verifyJsxClosingTag ( map ) { for ( const markerName in map ) { this . goToMarker ( markerName ) ; const actual = this . languageService . getJsxClosingTagAtPosition ( this . activeFile . fileName , this . currentCaretPosition ) ; assert . deepEqual ( actual , map [ markerName ] , markerName ) ; } } public verifyMatchingBracePosition ( bracePosition , expectedMatchPosition ) { const actual = this . languageService . getBraceMatchingAtPosition ( this . activeFile . fileName , bracePosition ) ; if ( actual . length !== 2 ) { this . raiseError ( `verifyMatchingBracePosition failed - expected result to contain 2 spans, but it had ${ actual . length }` ) ; } let actualMatchPosition = - 1 ; if ( bracePosition === actual [ 0 ] . start ) { actualMatchPosition = actual [ 1 ] . start ; } else if ( bracePosition === actual [ 1 ] . start ) { actualMatchPosition = actual [ 0 ] . start ; } else { this . raiseError ( `verifyMatchingBracePosition failed - could not find the brace position: ${ bracePosition } in the returned list: (${ actual [ 0 ] . start },${ ts . textSpanEnd ( actual [ 0 ] ) }) and (${ actual [ 1 ] . start },${ ts . textSpanEnd ( actual [ 1 ] ) })` ) ; } if ( actualMatchPosition !== expectedMatchPosition ) { this . raiseError ( `verifyMatchingBracePosition failed - expected: ${ actualMatchPosition },  actual: ${ expectedMatchPosition }` ) ; } } public verifyNoMatchingBracePosition ( bracePosition ) { const actual = this . languageService . getBraceMatchingAtPosition ( this . activeFile . fileName , bracePosition ) ; if ( actual . length !== 0 ) { this . raiseError ( "verifyNoMatchingBracePosition failed - expected: 0 spans, actual: " + actual . length ) ; } } public verifySpanOfEnclosingComment ( negative , onlyMultiLineDiverges ? ) { const expected = ! negative ; const position = this . currentCaretPosition ; const fileName = this . activeFile . fileName ; const actual = ! ! this . languageService . getSpanOfEnclosingComment ( fileName , position , false ) ; const actualOnlyMultiLine = ! ! this . languageService . getSpanOfEnclosingComment ( fileName , position , true ) ; if ( expected !== actual || onlyMultiLineDiverges === ( actual === actualOnlyMultiLine ) ) { this . raiseError ( `verifySpanOfEnclosingComment failed:
                position: '${ position }'
                fileName: '${ fileName }'
                onlyMultiLineDiverges: '${ onlyMultiLineDiverges }'
                actual: '${ actual }'
                actualOnlyMultiLine: '${ actualOnlyMultiLine }'
                expected: '${ expected }'.` ) ; } } public verifyNavigateTo ( options ) { for ( const { pattern , expected , fileName } of options ) { const items = this . languageService . getNavigateToItems ( pattern , undefined , fileName ) ; this . assertObjectsEqual ( items , expected . map ( ( e ) : ts . NavigateToItem => ( { name : e . name , kind : e . kind , kindModifiers : e . kindModifiers || "" , matchKind : e . matchKind || "exact" , isCaseSensitive : e . isCaseSensitive === undefined ? true : e . isCaseSensitive , fileName : e . range . fileName , textSpan : ts . createTextSpanFromRange ( e . range ) , containerName : e . containerName || "" , containerKind : e . containerKind || ts . ScriptElementKind . unknown , } ) ) ) ; } } public verifyNavigationBar ( json , options ) { this . verifyNavigationTreeOrBar ( json , this . languageService . getNavigationBarItems ( this . activeFile . fileName ) , "Bar" , options ) ; } public verifyNavigationTree ( json , options ) { this . verifyNavigationTreeOrBar ( json , this . languageService . getNavigationTree ( this . activeFile . fileName ) , "Tree" , options ) ; } private verifyNavigationTreeOrBar ( json , tree , name , options ) { if ( JSON . stringify ( tree , replacer ) !== JSON . stringify ( json ) ) { this . raiseError ( `verifyNavigation${ name } failed - \n${ showTextDiff ( stringify ( json ) , stringify ( tree , replacer ) ) }` ) ; } function replacer ( key , value ) { switch ( key ) { case "spans" : case "nameSpan" : return options && options . checkSpans ? value : undefined ; case "start" : case "length" : return value ; case "childItems" : return ! value || value . length === 0 ? undefined : value ; default : return value || undefined ; } } } public printNavigationItems ( searchValue ) { const items = this . languageService . getNavigateToItems ( searchValue ) ; Harness . IO . log ( `NavigationItems list (${ items . length } items)` ) ; for ( const item of items ) { Harness . IO . log ( `name: ${ item . name }, kind: ${ item . kind }, parentName: ${ item . containerName }, fileName: ${ item . fileName }` ) ; } } public printNavigationBar ( ) { const items = this . languageService . getNavigationBarItems ( this . activeFile . fileName ) ; Harness . IO . log ( `Navigation bar (${ items . length } items)` ) ; for ( const item of items ) { Harness . IO . log ( `${ ts . repeatString ( " " , item . indent ) }name: ${ item . text }, kind: ${ item . kind }, childItems: ${ item . childItems . map ( child => child . text ) }` ) ; } } private getOccurrencesAtCurrentPosition ( ) { return this . languageService . getOccurrencesAtPosition ( this . activeFile . fileName , this . currentCaretPosition ) ; } public verifyOccurrencesAtPositionListContains ( fileName , start , end , isWriteAccess ? ) { const occurrences = this . getOccurrencesAtCurrentPosition ( ) ; if ( ! occurrences || occurrences . length === 0 ) { return this . raiseError ( "verifyOccurrencesAtPositionListContains failed - found 0 references, expected at least one." ) ; } for ( const occurrence of occurrences ) { if ( occurrence && occurrence . fileName === fileName && occurrence . textSpan . start === start && ts . textSpanEnd ( occurrence . textSpan ) === end ) { if ( typeof isWriteAccess !== "undefined" && occurrence . isWriteAccess !== isWriteAccess ) { this . raiseError ( `verifyOccurrencesAtPositionListContains failed - item isWriteAccess value does not match, actual: ${ occurrence . isWriteAccess }, expected: ${ isWriteAccess }.` ) ; } return ; } } const missingItem = { fileName , start , end , isWriteAccess } ; this . raiseError ( `verifyOccurrencesAtPositionListContains failed - could not find the item: ${ stringify ( missingItem ) } in the returned list: (${ stringify ( occurrences ) })` ) ; } public verifyOccurrencesAtPositionListCount ( expectedCount ) { const occurrences = this . getOccurrencesAtCurrentPosition ( ) ; const actualCount = occurrences ? occurrences . length : 0 ; if ( expectedCount !== actualCount ) { this . raiseError ( `verifyOccurrencesAtPositionListCount failed - actual: ${ actualCount }, expected:${ expectedCount }` ) ; } } private getDocumentHighlightsAtCurrentPosition ( fileNamesToSearch ) { const filesToSearch = fileNamesToSearch . map ( name => ts . combinePaths ( this . basePath , name ) ) ; return this . languageService . getDocumentHighlights ( this . activeFile . fileName , this . currentCaretPosition , filesToSearch ) ; } public verifyRangesAreOccurrences ( isWriteAccess ? , ranges ? ) { ranges = ranges || this . getRanges ( ) ; assert ( ranges . length ) ; for ( const r of ranges ) { this . goToRangeStart ( r ) ; this . verifyOccurrencesAtPositionListCount ( ranges . length ) ; for ( const range of ranges ) { this . verifyOccurrencesAtPositionListContains ( range . fileName , range . pos , range . end , isWriteAccess ) ; } } } public verifyRangesWithSameTextAreRenameLocations ( ... texts ) { if ( texts . length ) { texts . forEach ( text => this . verifyRangesAreRenameLocations ( this . rangesByText ( ) . get ( text ) ) ) ; } else { this . rangesByText ( ) . forEach ( ranges => this . verifyRangesAreRenameLocations ( ranges ) ) ; } } public verifyRangesWithSameTextAreDocumentHighlights ( ) { this . rangesByText ( ) . forEach ( ranges => this . verifyRangesAreDocumentHighlights ( ranges , undefined ) ) ; } public verifyDocumentHighlightsOf ( startRange , ranges , options ) { const fileNames = options && options . filesToSearch || unique ( ranges , range => range . fileName ) ; this . goToRangeStart ( startRange ) ; this . verifyDocumentHighlights ( ranges , fileNames ) ; } public verifyRangesAreDocumentHighlights ( ranges , options ) { ranges = ranges || this . getRanges ( ) ; assert ( ranges . length ) ; const fileNames = options && options . filesToSearch || unique ( ranges , range => range . fileName ) ; for ( const range of ranges ) { this . goToRangeStart ( range ) ; this . verifyDocumentHighlights ( ranges , fileNames ) ; } } public verifyNoDocumentHighlights ( startRange ) { this . goToRangeStart ( startRange ) ; const documentHighlights = this . getDocumentHighlightsAtCurrentPosition ( [ this . activeFile . fileName ] ) ; const numHighlights = ts . length ( documentHighlights ) ; if ( numHighlights > 0 ) { this . raiseError ( `verifyNoDocumentHighlights failed - unexpectedly got ${ numHighlights } highlights` ) ; } } private verifyDocumentHighlights ( expectedRanges , fileNames = [ this . activeFile . fileName ] ) { fileNames = ts . map ( fileNames , ts . normalizePath ) ; const documentHighlights = this . getDocumentHighlightsAtCurrentPosition ( fileNames ) || [ ] ; for ( const dh of documentHighlights ) { if ( fileNames . indexOf ( dh . fileName ) === - 1 ) { this . raiseError ( `verifyDocumentHighlights failed - got highlights in unexpected file name ${ dh . fileName }` ) ; } } for ( const fileName of fileNames ) { const expectedRangesInFile = expectedRanges . filter ( r => ts . normalizePath ( r . fileName ) === fileName ) ; const highlights = ts . find ( documentHighlights , dh => dh . fileName === fileName ) ; const spansInFile = highlights ? highlights . highlightSpans . sort ( ( s1 , s2 ) => s1 . textSpan . start - s2 . textSpan . start ) : [ ] ; if ( expectedRangesInFile . length !== spansInFile . length ) { this . raiseError ( `verifyDocumentHighlights failed - In ${ fileName }, expected ${ expectedRangesInFile . length } highlights, got ${ spansInFile . length }` ) ; } ts . zipWith ( expectedRangesInFile , spansInFile , ( expectedRange , span ) => { if ( span . textSpan . start !== expectedRange . pos || ts . textSpanEnd ( span . textSpan ) !== expectedRange . end ) { this . raiseError ( `verifyDocumentHighlights failed - span does not match, actual: ${ stringify ( span . textSpan ) }, expected: ${ expectedRange . pos }--${ expectedRange . end }` ) ; } } ) ; } } public verifyCodeFixAvailable ( negative , expected ) { const codeFixes = this . getCodeFixes ( this . activeFile . fileName ) ; if ( negative ) { if ( typeof expected === "undefined" ) { this . assertObjectsEqual ( codeFixes , ts . emptyArray ) ; } else if ( typeof expected === "string" ) { if ( codeFixes . some ( fix => fix . fixName === expected ) ) { this . raiseError ( `Expected not to find a fix with the name '${ expected }', but one exists.` ) ; } } else { assert ( typeof expected === "undefined" || typeof expected === "string" , "With a negated assertion, 'expected' must be undefined or a string value of a codefix name." ) ; } } else if ( typeof expected === "string" ) { this . assertObjectsEqual ( codeFixes . map ( fix => fix . fixName ) , [ expected ] ) ; } else { const actuals = codeFixes . map ( ( fix ) : FourSlashInterface . VerifyCodeFixAvailableOptions => ( { description : fix . description , commands : fix . commands } ) ) ; this . assertObjectsEqual ( actuals , negative ? ts . emptyArray : expected ) ; } } public verifyCodeFixAllAvailable ( negative , fixName ) { const availableFixes = this . getCodeFixes ( this . activeFile . fileName ) ; const hasFix = availableFixes . some ( fix => fix . fixName === fixName && fix . fixId ) ; if ( negative && hasFix ) { this . raiseError ( `Expected not to find a fix with the name '${ fixName }', but one exists.` ) ; } else if ( ! negative && ! hasFix ) { if ( availableFixes . some ( fix => fix . fixName === fixName ) ) { this . raiseError ( `Found a fix with the name '${ fixName }', but fix-all is not available.` ) ; } this . raiseError ( `Expected to find a fix with the name '${ fixName }', but none exists.` + availableFixes . length ? ` Available fixes: ${ availableFixes . map ( fix => `${ fix . fixName } (${ fix . fixId ? "with" : "without" } fix-all)` ) . join ( ", " ) }` : "" ) ; } } public verifyApplicableRefactorAvailableAtMarker ( negative , markerName ) { const isAvailable = this . getApplicableRefactors ( this . getMarkerByName ( markerName ) ) . length > 0 ; if ( negative && isAvailable ) { this . raiseError ( `verifyApplicableRefactorAvailableAtMarker failed - expected no refactor at marker ${ markerName } but found some.` ) ; } if ( ! negative && ! isAvailable ) { this . raiseError ( `verifyApplicableRefactorAvailableAtMarker failed - expected a refactor at marker ${ markerName } but found none.` ) ; } } private getSelection ( ) { return { pos : this . currentCaretPosition , end : this . selectionEnd === - 1 ? this . currentCaretPosition : this . selectionEnd } ; } public verifyRefactorAvailable ( negative , triggerReason , name , actionName ? ) { let refactors = this . getApplicableRefactorsAtSelection ( triggerReason ) ; refactors = refactors . filter ( r => r . name === name && ( actionName === undefined || r . actions . some ( a => a . name === actionName ) ) ) ; const isAvailable = refactors . length > 0 ; if ( negative ) { if ( isAvailable ) { this . raiseError ( `verifyApplicableRefactorAvailableForRange failed - expected no refactor but found: ${ refactors . map ( r => r . name ) . join ( ", " ) }` ) ; } } else { if ( ! isAvailable ) { this . raiseError ( `verifyApplicableRefactorAvailableForRange failed - expected a refactor but found none.` ) ; } if ( refactors . length > 1 ) { this . raiseError ( `${ refactors . length } available refactors both have name ${ name } and action ${ actionName }` ) ; } } } public verifyRefactorKindsAvailable ( kind , expected , preferences = ts . emptyOptions ) { const refactors = this . getApplicableRefactorsAtSelection ( "invoked" , kind , preferences ) ; const availableKinds = ts . flatMap ( refactors , refactor => refactor . actions ) . map ( action => action . kind ) ; assert . deepEqual ( availableKinds . sort ( ) , expected . sort ( ) , `Expected kinds to be equal` ) ; } public verifyRefactorsAvailable ( names ) { assert . deepEqual ( unique ( this . getApplicableRefactorsAtSelection ( ) , r => r . name ) , names ) ; } public verifyApplicableRefactorAvailableForRange ( negative ) { const ranges = this . getRanges ( ) ; if ( ! ( ranges && ranges . length === 1 ) ) { throw new Error ( "Exactly one refactor range is allowed per test." ) ; } const isAvailable = this . getApplicableRefactors ( ts . first ( ranges ) ) . length > 0 ; if ( negative && isAvailable ) { this . raiseError ( `verifyApplicableRefactorAvailableForRange failed - expected no refactor but found some.` ) ; } if ( ! negative && ! isAvailable ) { this . raiseError ( `verifyApplicableRefactorAvailableForRange failed - expected a refactor but found none.` ) ; } } public applyRefactor ( { refactorName , actionName , actionDescription , newContent : newContentWithRenameMarker , triggerReason } ) { const range = this . getSelection ( ) ; const refactors = this . getApplicableRefactorsAtSelection ( triggerReason ) ; const refactorsWithName = refactors . filter ( r => r . name === refactorName ) ; if ( refactorsWithName . length === 0 ) { this . raiseError ( `The expected refactor: ${ refactorName } is not available at the marker location.\nAvailable refactors: ${ refactors . map ( r => r . name ) }` ) ; } const action = ts . firstDefined ( refactorsWithName , refactor => refactor . actions . find ( a => a . name === actionName ) ) ; if ( ! action ) { throw this . raiseError ( `The expected action: ${ actionName } is not included in: ${ ts . flatMap ( refactorsWithName , r => r . actions . map ( a => a . name ) ) }` ) ; } if ( action . description !== actionDescription ) { this . raiseError ( `Expected action description to be ${ JSON . stringify ( actionDescription ) }, got: ${ JSON . stringify ( action . description ) }` ) ; } const editInfo = this . languageService . getEditsForRefactor ( this . activeFile . fileName , this . formatCodeSettings , range , refactorName , actionName , ts . emptyOptions ) ! ; for ( const edit of editInfo . edits ) { this . applyEdits ( edit . fileName , edit . textChanges ) ; } let renameFilename ; let renamePosition ; const newFileContents = typeof newContentWithRenameMarker === "string" ? { [ this . activeFile . fileName ] : newContentWithRenameMarker } : newContentWithRenameMarker ; for ( const fileName in newFileContents ) { const { renamePosition : rp , newContent } = TestState . parseNewContent ( newFileContents [ fileName ] ) ; if ( renamePosition === undefined ) { renameFilename = fileName ; renamePosition = rp ; } else { ts . Debug . assert ( rp === undefined ) ; } this . verifyFileContent ( fileName , newContent ) ; } if ( renamePosition === undefined ) { if ( editInfo . renameLocation !== undefined ) { this . raiseError ( `Did not expect a rename location, got ${ editInfo . renameLocation }` ) ; } } else { this . assertObjectsEqual ( editInfo . renameFilename , renameFilename ) ; if ( renamePosition !== editInfo . renameLocation ) { this . raiseError ( `Expected rename position of ${ renamePosition }, but got ${ editInfo . renameLocation }` ) ; } } } private static parseNewContent ( newContentWithRenameMarker ) { const renamePosition = newContentWithRenameMarker . indexOf ( "/*RENAME*/" ) ; if ( renamePosition === - 1 ) { return { renamePosition : undefined , newContent : newContentWithRenameMarker } ; } else { const newContent = newContentWithRenameMarker . slice ( 0 , renamePosition ) + newContentWithRenameMarker . slice ( renamePosition + "/*RENAME*/" . length ) ; return { renamePosition , newContent } ; } } public noMoveToNewFile ( ) { const ranges = this . getRanges ( ) ; assert ( ranges . length ) ; for ( const range of ranges ) { for ( const refactor of this . getApplicableRefactors ( range , { allowTextChangesInNewFiles : true } ) ) { if ( refactor . name === "Move to a new file" ) { ts . Debug . fail ( "Did not expect to get 'move to a new file' refactor" ) ; } } } } public moveToNewFile ( options ) { assert ( this . getRanges ( ) . length === 1 , "Must have exactly one fourslash range (source enclosed between '[|' and '|]' delimiters) in the source file" ) ; const range = this . getRanges ( ) [ 0 ] ; const refactor = ts . find ( this . getApplicableRefactors ( range , { allowTextChangesInNewFiles : true } ) , r => r . name === "Move to a new file" ) ! ; assert ( refactor . actions . length === 1 ) ; const action = ts . first ( refactor . actions ) ; assert ( action . name === "Move to a new file" && action . description === "Move to a new file" ) ; const editInfo = this . languageService . getEditsForRefactor ( range . fileName , this . formatCodeSettings , range , refactor . name , action . name , options . preferences || ts . emptyOptions ) ! ; this . verifyNewContent ( { newFileContent : options . newFileContents } , editInfo . edits ) ; } private testNewFileContents ( edits , newFileContents , description ) { for ( const { fileName , textChanges } of edits ) { const newContent = newFileContents [ fileName ] ; if ( newContent === undefined ) { this . raiseError ( `${ description } - There was an edit in ${ fileName } but new content was not specified.` ) ; } const fileContent = this . tryGetFileContent ( fileName ) ; if ( fileContent !== undefined ) { const actualNewContent = ts . textChanges . applyChanges ( fileContent , textChanges ) ; assert . equal ( actualNewContent , newContent , `new content for ${ fileName }` ) ; } else { assert ( textChanges . length === 1 ) ; const change = ts . first ( textChanges ) ; assert . deepEqual ( change . span , ts . createTextSpan ( 0 , 0 ) ) ; assert . equal ( change . newText , newContent , `${ description } - Content for ${ fileName }` ) ; } } for ( const fileName in newFileContents ) { if ( ! edits . some ( e => e . fileName === fileName ) ) { ts . Debug . fail ( `${ description } - Asserted new contents of ${ fileName } but there were no edits` ) ; } } } public verifyFileAfterApplyingRefactorAtMarker ( markerName , expectedContent , refactorNameToApply , actionName , formattingOptions ? ) { formattingOptions = formattingOptions || this . formatCodeSettings ; const marker = this . getMarkerByName ( markerName ) ; const applicableRefactors = this . languageService . getApplicableRefactors ( this . activeFile . fileName , marker . position , ts . emptyOptions ) ; const applicableRefactorToApply = ts . find ( applicableRefactors , refactor => refactor . name === refactorNameToApply ) ; if ( ! applicableRefactorToApply ) { this . raiseError ( `The expected refactor: ${ refactorNameToApply } is not available at the marker location.` ) ; } const editInfo = this . languageService . getEditsForRefactor ( marker . fileName , formattingOptions , marker . position , refactorNameToApply , actionName , ts . emptyOptions ) ! ; for ( const edit of editInfo . edits ) { this . applyEdits ( edit . fileName , edit . textChanges ) ; } const actualContent = this . getFileContent ( marker . fileName ) ; if ( actualContent !== expectedContent ) { this . raiseError ( `verifyFileAfterApplyingRefactors failed:\n${ showTextDiff ( expectedContent , actualContent ) }` ) ; } } public printAvailableCodeFixes ( ) { const codeFixes = this . getCodeFixes ( this . activeFile . fileName ) ; Harness . IO . log ( stringify ( codeFixes ) ) ; } private formatCallHierarchyItemSpan ( file , span , prefix , trailingPrefix = prefix ) { const startLc = this . languageServiceAdapterHost . positionToLineAndCharacter ( file . fileName , span . start ) ; const endLc = this . languageServiceAdapterHost . positionToLineAndCharacter ( file . fileName , ts . textSpanEnd ( span ) ) ; const lines = this . spanLines ( file , span , { fullLines : true , lineNumbers : true , selection : true } ) ; let text = "" ; text += `${ prefix }╭ ${ file . fileName }:${ startLc . line + 1 }:${ startLc . character + 1 }-${ endLc . line + 1 }:${ endLc . character + 1 }\n` ; for ( const line of lines ) { text += `${ prefix }│ ${ line . trimRight ( ) }\n` ; } text += `${ trailingPrefix }╰\n` ; return text ; } private formatCallHierarchyItemSpans ( file , spans , prefix , trailingPrefix = prefix ) { let text = "" ; for ( let i = 0 ; i < spans . length ; i ++ ) { text += this . formatCallHierarchyItemSpan ( file , spans [ i ] , prefix , i < spans . length - 1 ? prefix : trailingPrefix ) ; } return text ; } private formatCallHierarchyItem ( file , callHierarchyItem , direction , seen , prefix , trailingPrefix = prefix ) { const key = `${ callHierarchyItem . file }|${ JSON . stringify ( callHierarchyItem . span ) }|${ direction }` ; const alreadySeen = seen . has ( key ) ; seen . set ( key , true ) ; const incomingCalls = direction === CallHierarchyItemDirection . Outgoing ? { result : "skip" } as const : alreadySeen ? { result : "seen" } as const : { result : "show" , values : this . languageService . provideCallHierarchyIncomingCalls ( callHierarchyItem . file , callHierarchyItem . selectionSpan . start ) } as const ; const outgoingCalls = direction === CallHierarchyItemDirection . Incoming ? { result : "skip" } as const : alreadySeen ? { result : "seen" } as const : { result : "show" , values : this . languageService . provideCallHierarchyOutgoingCalls ( callHierarchyItem . file , callHierarchyItem . selectionSpan . start ) } as const ; let text = "" ; text += `${ prefix }╭ name: ${ callHierarchyItem . name }\n` ; text += `${ prefix }├ kind: ${ callHierarchyItem . kind }\n` ; if ( callHierarchyItem . containerName ) { text += `${ prefix }├ containerName: ${ callHierarchyItem . containerName }\n` ; } text += `${ prefix }├ file: ${ callHierarchyItem . file }\n` ; text += `${ prefix }├ span:\n` ; text += this . formatCallHierarchyItemSpan ( file , callHierarchyItem . span , `${ prefix }│ ` ) ; text += `${ prefix }├ selectionSpan:\n` ; text += this . formatCallHierarchyItemSpan ( file , callHierarchyItem . selectionSpan , `${ prefix }│ ` , incomingCalls . result !== "skip" || outgoingCalls . result !== "skip" ? `${ prefix }│ ` : `${ trailingPrefix }╰ ` ) ; if ( incomingCalls . result === "seen" ) { if ( outgoingCalls . result === "skip" ) { text += `${ trailingPrefix }╰ incoming: ...\n` ; } else { text += `${ prefix }├ incoming: ...\n` ; } } else if ( incomingCalls . result === "show" ) { if ( ! ts . some ( incomingCalls . values ) ) { if ( outgoingCalls . result === "skip" ) { text += `${ trailingPrefix }╰ incoming: none\n` ; } else { text += `${ prefix }├ incoming: none\n` ; } } else { text += `${ prefix }├ incoming:\n` ; for ( let i = 0 ; i < incomingCalls . values . length ; i ++ ) { const incomingCall = incomingCalls . values [ i ] ; const file = this . findFile ( incomingCall . from . file ) ; text += `${ prefix }│ ╭ from:\n` ; text += this . formatCallHierarchyItem ( file , incomingCall . from , CallHierarchyItemDirection . Incoming , seen , `${ prefix }│ │ ` ) ; text += `${ prefix }│ ├ fromSpans:\n` ; text += this . formatCallHierarchyItemSpans ( file , incomingCall . fromSpans , `${ prefix }│ │ ` , i < incomingCalls . values . length - 1 ? `${ prefix }│ ╰ ` : outgoingCalls . result !== "skip" ? `${ prefix }│ ╰ ` : `${ trailingPrefix }╰ ╰ ` ) ; } } } if ( outgoingCalls . result === "seen" ) { text += `${ trailingPrefix }╰ outgoing: ...\n` ; } else if ( outgoingCalls . result === "show" ) { if ( ! ts . some ( outgoingCalls . values ) ) { text += `${ trailingPrefix }╰ outgoing: none\n` ; } else { text += `${ prefix }├ outgoing:\n` ; for ( let i = 0 ; i < outgoingCalls . values . length ; i ++ ) { const outgoingCall = outgoingCalls . values [ i ] ; text += `${ prefix }│ ╭ to:\n` ; text += this . formatCallHierarchyItem ( this . findFile ( outgoingCall . to . file ) , outgoingCall . to , CallHierarchyItemDirection . Outgoing , seen , `${ prefix }│ │ ` ) ; text += `${ prefix }│ ├ fromSpans:\n` ; text += this . formatCallHierarchyItemSpans ( file , outgoingCall . fromSpans , `${ prefix }│ │ ` , i < outgoingCalls . values . length - 1 ? `${ prefix }│ ╰ ` : `${ trailingPrefix }╰ ╰ ` ) ; } } } return text ; } private formatCallHierarchy ( callHierarchyItem ) { let text = "" ; if ( callHierarchyItem ) { const file = this . findFile ( callHierarchyItem . file ) ; text += this . formatCallHierarchyItem ( file , callHierarchyItem , CallHierarchyItemDirection . Root , new ts . Map ( ) , "" ) ; } return text ; } public baselineCallHierarchy ( ) { const baselineFile = this . getBaselineFileNameForContainingTestFile ( ".callHierarchy.txt" ) ; const callHierarchyItem = this . languageService . prepareCallHierarchy ( this . activeFile . fileName , this . currentCaretPosition ) ; const text = callHierarchyItem ? ts . mapOneOrMany ( callHierarchyItem , item => this . formatCallHierarchy ( item ) , result => result . join ( "" ) ) : "none" ; Harness . Baseline . runBaseline ( baselineFile , text ) ; } private assertTextSpanEqualsRange ( span , range , message ? ) { if ( ! textSpanEqualsRange ( span , range ) ) { this . raiseError ( `${ prefixMessage ( message ) }Expected to find TextSpan ${ JSON . stringify ( { start : range . pos , length : range . end - range . pos } ) } but got ${ JSON . stringify ( span ) } instead.` ) ; } } private getLineContent ( index ) { const text = this . getFileContent ( this . activeFile . fileName ) ; const pos = this . languageServiceAdapterHost . lineAndCharacterToPosition ( this . activeFile . fileName , { line : index , character : 0 } ) ; let startPos = pos , endPos = pos ; while ( startPos > 0 ) { const ch = text . charCodeAt ( startPos - 1 ) ; if ( ch === ts . CharacterCodes . carriageReturn || ch === ts . CharacterCodes . lineFeed ) { break ; } startPos -- ; } while ( endPos < text . length ) { const ch = text . charCodeAt ( endPos ) ; if ( ch === ts . CharacterCodes . carriageReturn || ch === ts . CharacterCodes . lineFeed ) { break ; } endPos ++ ; } return text . substring ( startPos , endPos ) ; } private getCurrentLineContent ( ) { return this . getLineContent ( this . languageServiceAdapterHost . positionToLineAndCharacter ( this . activeFile . fileName , this . currentCaretPosition ) . line ) ; } private findFile ( indexOrName ) { if ( typeof indexOrName === "number" ) { const index = indexOrName ; if ( index >= this . testData . files . length ) { throw new Error ( `File index (${ index }) in openFile was out of range. There are only ${ this . testData . files . length } files in this test.` ) ; } else { return this . testData . files [ index ] ; } } else if ( ts . isString ( indexOrName ) ) { const { file , availableNames } = this . tryFindFileWorker ( indexOrName ) ; if ( ! file ) { throw new Error ( `No test file named "${ indexOrName }" exists. Available file names are: ${ availableNames . join ( ", " ) }` ) ; } return file ; } else { return ts . Debug . assertNever ( indexOrName ) ; } } private tryFindFileWorker ( name ) { name = ts . normalizePath ( name ) ; name = name . indexOf ( "/" ) === - 1 ? ( this . basePath + "/" + name ) : name ; const availableNames = [ ] ; const file = ts . forEach ( this . testData . files , file => { const fn = ts . normalizePath ( file . fileName ) ; if ( fn ) { if ( fn === name ) { return file ; } availableNames . push ( fn ) ; } } ) ; return { file , availableNames } ; } private hasFile ( name ) { return this . tryFindFileWorker ( name ) . file !== undefined ; } private getLineColStringAtPosition ( position , file = this . activeFile ) { const pos = this . languageServiceAdapterHost . positionToLineAndCharacter ( file . fileName , position ) ; return `line ${ ( pos . line + 1 ) }, col ${ pos . character }` ; } public getMarkerByName ( markerName ) { const markerPos = this . testData . markerPositions . get ( markerName ) ; if ( markerPos === undefined ) { throw new Error ( `Unknown marker "${ markerName }" Available markers: ${ this . getMarkerNames ( ) . map ( m => "\"" + m + "\"" ) . join ( ", " ) }` ) ; } else { return markerPos ; } } public setCancelled ( numberOfCalls ) { this . cancellationToken . setCancelled ( numberOfCalls ) ; } public resetCancelled ( ) { this . cancellationToken . resetCancelled ( ) ; } public getEditsForFileRename ( { oldPath , newPath , newFileContents , preferences } ) { const test = ( fileContents , description ) : void => { const changes = this . languageService . getEditsForFileRename ( oldPath , newPath , this . formatCodeSettings , preferences ) ; this . testNewFileContents ( changes , fileContents , description ) ; } ; ts . Debug . assert ( ! this . hasFile ( newPath ) , "initially, newPath should not exist" ) ; test ( newFileContents , "with file not yet moved" ) ; this . languageServiceAdapterHost . renameFileOrDirectory ( oldPath , newPath ) ; this . languageService . cleanupSemanticCache ( ) ; const pathUpdater = ts . getPathUpdater ( oldPath , newPath , ts . createGetCanonicalFileName ( false ) , undefined ) ; test ( renameKeys ( newFileContents , key => pathUpdater ( key ) || key ) , "with file moved" ) ; } private getApplicableRefactorsAtSelection ( triggerReason = "implicit" , kind ? , preferences = ts . emptyOptions ) { return this . getApplicableRefactorsWorker ( this . getSelection ( ) , this . activeFile . fileName , preferences , triggerReason , kind ) ; } private getApplicableRefactors ( rangeOrMarker , preferences = ts . emptyOptions , triggerReason = "implicit" , kind ? ) { return this . getApplicableRefactorsWorker ( "position" in rangeOrMarker ? rangeOrMarker . position : rangeOrMarker , rangeOrMarker . fileName , preferences , triggerReason , kind ) ; } private getApplicableRefactorsWorker ( positionOrRange , fileName , preferences = ts . emptyOptions , triggerReason , kind ? ) { return this . languageService . getApplicableRefactors ( fileName , positionOrRange , preferences , triggerReason , kind ) || ts . emptyArray ; } public configurePlugin ( pluginName , configuration ) { ( this . languageService as ts . server . SessionClient ) . configurePlugin ( pluginName , configuration ) ; } public setCompilerOptionsForInferredProjects ( options ) { ts . Debug . assert ( this . testType === FourSlashTestType . Server ) ; ( this . languageService as ts . server . SessionClient ) . setCompilerOptionsForInferredProjects ( options ) ; } public toggleLineComment ( newFileContent ) { const changes = [ ] ; for ( const range of this . getRanges ( ) ) { changes . push . apply ( changes , this . languageService . toggleLineComment ( this . activeFile . fileName , range ) ) ; } this . applyEdits ( this . activeFile . fileName , changes ) ; this . verifyCurrentFileContent ( newFileContent ) ; } public toggleMultilineComment ( newFileContent ) { const changes = [ ] ; for ( const range of this . getRanges ( ) ) { changes . push . apply ( changes , this . languageService . toggleMultilineComment ( this . activeFile . fileName , range ) ) ; } this . applyEdits ( this . activeFile . fileName , changes ) ; this . verifyCurrentFileContent ( newFileContent ) ; } public commentSelection ( newFileContent ) { const changes = [ ] ; for ( const range of this . getRanges ( ) ) { changes . push . apply ( changes , this . languageService . commentSelection ( this . activeFile . fileName , range ) ) ; } this . applyEdits ( this . activeFile . fileName , changes ) ; this . verifyCurrentFileContent ( newFileContent ) ; } public uncommentSelection ( newFileContent ) { const changes = [ ] ; for ( const range of this . getRanges ( ) ) { changes . push . apply ( changes , this . languageService . uncommentSelection ( this . activeFile . fileName , range ) ) ; } this . applyEdits ( this . activeFile . fileName , changes ) ; this . verifyCurrentFileContent ( newFileContent ) ; } } function prefixMessage ( message ) { return message ? `${ message } - ` : "" ; } function textSpanEqualsRange ( span , range ) { return span . start === range . pos && span . length === range . end - range . pos ; } function updateTextRangeForTextChanges ( { pos , end } , textChanges ) { forEachTextChange ( textChanges , change => { const update = ( p ) : number => updatePosition ( p , change . span . start , ts . textSpanEnd ( change . span ) , change . newText ) ; pos = update ( pos ) ; end = update ( end ) ; } ) ; return { pos , end } ; } /** Apply each textChange in order, updating future changes to account for the text offset of previous changes. */ function forEachTextChange ( changes , cb ) { changes = JSON . parse ( JSON . stringify ( changes ) ) ; for ( let i = 0 ; i < changes . length ; i ++ ) { const change = changes [ i ] ; cb ( change ) ; const changeDelta = change . newText . length - change . span . length ; for ( let j = i + 1 ; j < changes . length ; j ++ ) { if ( changes [ j ] . span . start >= change . span . start ) { changes [ j ] . span . start += changeDelta ; } } } } function updatePosition ( position , editStart , editEnd , { length } ) { return position <= editStart ? position : position < editEnd ? - 1 : position + length - + ( editEnd - editStart ) ; } function renameKeys < T > ( obj , renameKey ) { const res = { } ; for ( const key in obj ) { res [ renameKey ( key ) ] = obj [ key ] ; } return res ; } export function runFourSlashTest ( basePath , testType , fileName ) { const content = Harness . IO . readFile ( fileName ) ! ; runFourSlashTestContent ( basePath , testType , content , fileName ) ; } export function runFourSlashTestContent ( basePath , testType , content , fileName ) { const absoluteBasePath = ts . combinePaths ( Harness . virtualFileSystemRoot , basePath ) ; const absoluteFileName = ts . combinePaths ( Harness . virtualFileSystemRoot , fileName ) ; const testData = parseTestData ( absoluteBasePath , content , absoluteFileName ) ; const state = new TestState ( absoluteFileName , absoluteBasePath , testType , testData ) ; const actualFileName = Harness . IO . resolvePath ( fileName ) || absoluteFileName ; const output = ts . transpileModule ( content , { reportDiagnostics : true , fileName : actualFileName , compilerOptions : { target : ts . ScriptTarget . ES2015 , inlineSourceMap : true , inlineSources : true } } ) ; if ( output . diagnostics ! . length > 0 ) { throw new Error ( `Syntax error in ${ absoluteBasePath }: ${ output . diagnostics ! [ 0 ] . messageText }` ) ; } runCode ( output . outputText , state , actualFileName ) ; } function runCode ( code , state , fileName ) { const generatedFile = ts . changeExtension ( fileName , ".js" ) ; const wrappedCode = `(function(test, goTo, plugins, verify, edit, debug, format, cancellation, classification, completion, verifyOperationIsCancelled) {${ code }\n//# sourceURL=${ ts . getBaseFileName ( generatedFile ) }\n})` ; type SourceMapSupportModule = typeof import ( "source-map-support" ) & { resetRetrieveHandlers ( ) : void ; } ; let sourceMapSupportModule ; try { sourceMapSupportModule = require ( "source-map-support" ) ; } catch { } sourceMapSupportModule ?. install ( { retrieveFile : path => { return path === generatedFile ? wrappedCode : undefined ! ; } } ) ; try { const test = new FourSlashInterface . Test ( state ) ; const goTo = new FourSlashInterface . GoTo ( state ) ; const config = new FourSlashInterface . Config ( state ) ; const verify = new FourSlashInterface . Verify ( state ) ; const edit = new FourSlashInterface . Edit ( state ) ; const debug = new FourSlashInterface . Debug ( state ) ; const format = new FourSlashInterface . Format ( state ) ; const cancellation = new FourSlashInterface . Cancellation ( state ) ; const f = eval ( wrappedCode ) ; f ( test , goTo , config , verify , edit , debug , format , cancellation , FourSlashInterface . classification , FourSlashInterface . Completion , verifyOperationIsCancelled ) ; } catch ( err ) { err . stack ?. toString ( ) ; throw err ; } finally { sourceMapSupportModule ?. resetRetrieveHandlers ( ) ; } } function chompLeadingSpace ( content ) { const lines = content . split ( "\n" ) ; for ( const line of lines ) { if ( ( line . length !== 0 ) && ( line . charAt ( 0 ) !== " " ) ) { return content ; } } return lines . map ( s => s . substr ( 1 ) ) . join ( "\n" ) ; } function parseTestData ( basePath , contents , fileName ) { const optionRegex = /^\s*@(\w+):\s*(.*)\s*/ ; const files = [ ] ; const globalOptions = { } ; let symlinks ; const lines = contents . split ( "\n" ) ; let i = 0 ; const markerPositions = new ts . Map < string , Marker > ( ) ; const markers = [ ] ; const ranges = [ ] ; let currentFileContent ; let currentFileName = fileName ; let currentFileSymlinks ; let currentFileOptions = { } ; function nextFile ( ) { if ( currentFileContent === undefined ) return ; const file = parseFileContent ( currentFileContent , currentFileName , markerPositions , markers , ranges ) ; file . fileOptions = currentFileOptions ; file . symlinks = currentFileSymlinks ; files . push ( file ) ; currentFileContent = undefined ; currentFileOptions = { } ; currentFileName = fileName ; currentFileSymlinks = undefined ; } for ( let line of lines ) { i ++ ; if ( line . length > 0 && line . charAt ( line . length - 1 ) === "\r" ) { line = line . substr ( 0 , line . length - 1 ) ; } if ( line . substr ( 0 , 4 ) === "////" ) { const text = line . substr ( 4 ) ; currentFileContent = currentFileContent === undefined ? text : currentFileContent + "\n" + text ; } else if ( line . substr ( 0 , 3 ) === "///" && currentFileContent !== undefined ) { throw new Error ( "Three-slash line in the middle of four-slash region at line " + i ) ; } else if ( line . substr ( 0 , 2 ) === "//" ) { const possiblySymlinks = Harness . TestCaseParser . parseSymlinkFromTest ( line , symlinks ) ; if ( possiblySymlinks ) { symlinks = possiblySymlinks ; } else { const match = optionRegex . exec ( line . substr ( 2 ) ) ; if ( match ) { const key = match [ 1 ] . toLowerCase ( ) ; const value = match [ 2 ] ; if ( ! ts . contains ( fileMetadataNames , key ) ) { if ( globalOptions [ key ] !== undefined ) { throw new Error ( `Global option '${ key }' already exists` ) ; } globalOptions [ key ] = value ; } else { switch ( key ) { case MetadataOptionNames . fileName : nextFile ( ) ; currentFileName = ts . isRootedDiskPath ( value ) ? value : basePath + "/" + value ; currentFileOptions [ key ] = value ; break ; case MetadataOptionNames . symlink : currentFileSymlinks = ts . append ( currentFileSymlinks , value ) ; break ; default : currentFileOptions [ key ] = value ; } } } } } else if ( line !== "" ) { nextFile ( ) ; } } const config = ts . find ( files , isConfig ) ; if ( config ) { let directive = getNonFileNameOptionInFileList ( files ) ; if ( ! directive ) { directive = getNonFileNameOptionInObject ( globalOptions ) ; } if ( directive ) { throw Error ( `It is not allowed to use ${ config . fileName } along with directive '${ directive }'` ) ; } } return { markerPositions , markers , globalOptions , files , symlinks , ranges } ; } function isConfig ( file ) { return Harness . getConfigNameFromFileName ( file . fileName ) !== undefined ; } function getNonFileNameOptionInFileList ( files ) { return ts . forEach ( files , f => getNonFileNameOptionInObject ( f . fileOptions ) ) ; } function getNonFileNameOptionInObject ( optionObject ) { for ( const option in optionObject ) { switch ( option ) { case MetadataOptionNames . fileName : case MetadataOptionNames . baselineFile : case MetadataOptionNames . emitThisFile : break ; default : return option ; } } return undefined ; } const enum State { none , inSlashStarMarker , inObjectMarker } function reportError ( fileName , line , col , message ) { const errorMessage = fileName + "(" + line + "," + col + "): " + message ; throw new Error ( errorMessage ) ; } function recordObjectMarker ( fileName , location , text , markerMap , markers ) { let markerValue ; try { markerValue = JSON . parse ( "{ " + text + " }" ) ; } catch ( e ) { reportError ( fileName , location . sourceLine , location . sourceColumn , "Unable to parse marker text " + e . message ) ; } if ( markerValue === undefined ) { reportError ( fileName , location . sourceLine , location . sourceColumn , "Object markers can not be empty" ) ; return undefined ; } const marker = { fileName , position : location . position , data : markerValue } ; if ( markerValue . name ) { markerMap . set ( markerValue . name , marker ) ; } markers . push ( marker ) ; return marker ; } function recordMarker ( fileName , location , name , markerMap , markers ) { const marker = { fileName , position : location . position } ; if ( markerMap . has ( name ) ) { const message = "Marker '" + name + "' is duplicated in the source file contents." ; reportError ( marker . fileName , location . sourceLine , location . sourceColumn , message ) ; return undefined ; } else { markerMap . set ( name , marker ) ; markers . push ( marker ) ; return marker ; } } function parseFileContent ( content , fileName , markerMap , markers , ranges ) { content = chompLeadingSpace ( content ) ; const validMarkerChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz$1234567890_" ; let output = "" ; let openMarker ; const openRanges = [ ] ; let localRanges = [ ] ; let lastNormalCharPosition = 0 ; let difference = 0 ; let state = State . none ; let line = 1 ; let column = 1 ; const flush = ( lastSafeCharIndex ) => { output = output + content . substr ( lastNormalCharPosition , lastSafeCharIndex === undefined ? undefined : lastSafeCharIndex - lastNormalCharPosition ) ; } ; if ( content . length > 0 ) { let previousChar = content . charAt ( 0 ) ; for ( let i = 1 ; i < content . length ; i ++ ) { const currentChar = content . charAt ( i ) ; switch ( state ) { case State . none : if ( previousChar === "[" && currentChar === "|" ) { openRanges . push ( { position : ( i - 1 ) - difference , sourcePosition : i - 1 , sourceLine : line , sourceColumn : column , } ) ; flush ( i - 1 ) ; lastNormalCharPosition = i + 1 ; difference += 2 ; } else if ( previousChar === "|" && currentChar === "]" ) { const rangeStart = openRanges . pop ( ) ; if ( ! rangeStart ) { throw reportError ( fileName , line , column , "Found range end with no matching start." ) ; } const range = { fileName , pos : rangeStart . position , end : ( i - 1 ) - difference , marker : rangeStart . marker } ; localRanges . push ( range ) ; flush ( i - 1 ) ; lastNormalCharPosition = i + 1 ; difference += 2 ; } else if ( previousChar === "/" && currentChar === "*" ) { state = State . inSlashStarMarker ; openMarker = { position : ( i - 1 ) - difference , sourcePosition : i - 1 , sourceLine : line , sourceColumn : column , } ; } else if ( previousChar === "{" && currentChar === "|" ) { state = State . inObjectMarker ; openMarker = { position : ( i - 1 ) - difference , sourcePosition : i - 1 , sourceLine : line , sourceColumn : column , } ; flush ( i - 1 ) ; } break ; case State . inObjectMarker : if ( previousChar === "|" && currentChar === "}" ) { const objectMarkerNameText = content . substring ( openMarker ! . sourcePosition + 2 , i - 1 ) . trim ( ) ; const marker = recordObjectMarker ( fileName , openMarker ! , objectMarkerNameText , markerMap , markers ) ; if ( openRanges . length > 0 ) { openRanges [ openRanges . length - 1 ] . marker = marker ; } lastNormalCharPosition = i + 1 ; difference += i + 1 - openMarker ! . sourcePosition ; openMarker = undefined ; state = State . none ; } break ; case State . inSlashStarMarker : if ( previousChar === "*" && currentChar === "/" ) { const markerNameText = content . substring ( openMarker ! . sourcePosition + 2 , i - 1 ) . trim ( ) ; const marker = recordMarker ( fileName , openMarker ! , markerNameText , markerMap , markers ) ; if ( openRanges . length > 0 ) { openRanges [ openRanges . length - 1 ] . marker = marker ; } flush ( openMarker ! . sourcePosition ) ; lastNormalCharPosition = i + 1 ; difference += i + 1 - openMarker ! . sourcePosition ; openMarker = undefined ; state = State . none ; } else if ( validMarkerChars . indexOf ( currentChar ) < 0 ) { if ( currentChar === "*" && i < content . length - 1 && content . charAt ( i + 1 ) === "/" ) { } else { flush ( i ) ; lastNormalCharPosition = i ; openMarker = undefined ; state = State . none ; } } break ; } if ( currentChar === "\n" && previousChar === "\r" ) { continue ; } else if ( currentChar === "\n" || currentChar === "\r" ) { line ++ ; column = 1 ; continue ; } column ++ ; previousChar = currentChar ; } } flush ( undefined ) ; if ( openRanges . length > 0 ) { const openRange = openRanges [ 0 ] ; reportError ( fileName , openRange . sourceLine , openRange . sourceColumn , "Unterminated range." ) ; } if ( openMarker ) { reportError ( fileName , openMarker . sourceLine , openMarker . sourceColumn , "Unterminated marker." ) ; } localRanges = localRanges . sort ( ( a , b ) => a . pos < b . pos ? - 1 : a . pos === b . pos && a . end > b . end ? - 1 : 1 ) ; localRanges . forEach ( r => ranges . push ( r ) ) ; return { content : output , fileOptions : { } , version : 0 , fileName , } ; } function stringify ( data , replacer ? ) { return JSON . stringify ( data , replacer , 2 ) ; } /** Collects an array of unique outputs. */ function unique < T > ( inputs , getOutput ) { const set = new ts . Map < string , true > ( ) ; for ( const input of inputs ) { const out = getOutput ( input ) ; set . set ( out , true ) ; } return ts . arrayFrom ( set . keys ( ) ) ; } function toArray < T > ( x ) { return ts . isArray ( x ) ? x : [ x ] ; } function makeWhitespaceVisible ( text ) { return text . replace ( / /g , "\u00B7" ) . replace ( /\r/g , "\u00B6" ) . replace ( /\n/g , "\u2193\n" ) . replace ( /\t/g , "\u2192\   " ) ; } function showTextDiff ( expected , actual ) { if ( differOnlyByWhitespace ( expected , actual ) ) { expected = makeWhitespaceVisible ( expected ) ; actual = makeWhitespaceVisible ( actual ) ; } return displayExpectedAndActualString ( expected , actual ) ; } function differOnlyByWhitespace ( a , b ) { return stripWhitespace ( a ) === stripWhitespace ( b ) ; } function stripWhitespace ( s ) { return s . replace ( /\s/g , "" ) ; } function findDuplicatedElement < T > ( a , equal ) { for ( let i = 0 ; i < a . length ; i ++ ) { for ( let j = i + 1 ; j < a . length ; j ++ ) { if ( equal ( a [ i ] , a [ j ] ) ) { return a [ i ] ; } } } } function displayExpectedAndActualString ( expected , actual , quoted = false ) { const expectMsg = "\x1b[1mExpected\x1b[0m\x1b[31m" ; const actualMsg = "\x1b[1mActual\x1b[0m\x1b[31m" ; const expectedString = quoted ? "\"" + expected + "\"" : expected ; const actualString = quoted ? "\"" + actual + "\"" : actual ; return `\n${ expectMsg }:\n${ expectedString }\n\n${ actualMsg }:\n${ highlightDifferenceBetweenStrings ( expected , actualString ) }` ; } function templateToRegExp ( template ) { return new RegExp ( `^${ ts . regExpEscape ( template ) . replace ( /\\\{\d+\\\}/g , ".*?" ) }$` ) ; } function rangesOfDiffBetweenTwoStrings ( source , target ) { const ranges = [ ] as { start : number ; length : number ; } [ ] ; const addToIndex = ( index ) => { const closestIndex = ranges [ ranges . length - 1 ] ; if ( closestIndex ) { const doesAddToIndex = closestIndex . start + closestIndex . length === index - 1 ; if ( doesAddToIndex ) { closestIndex . length = closestIndex . length + 1 ; } else { ranges . push ( { start : index - 1 , length : 1 } ) ; } } else { ranges . push ( { start : index - 1 , length : 1 } ) ; } } ; for ( let index = 0 ; index < Math . max ( source . length , target . length ) ; index ++ ) { const srcChar = source [ index ] ; const targetChar = target [ index ] ; if ( srcChar !== targetChar ) addToIndex ( index ) ; } return ranges ; } function highlightDifferenceBetweenStrings ( source , target ) { const ranges = rangesOfDiffBetweenTwoStrings ( source , target ) ; let emTarget = target ; ranges . forEach ( ( range , index ) => { const lhs = `\x1b[4m` ; const rhs = `\x1b[0m\x1b[31m` ; const additionalOffset = index * lhs . length + index * rhs . length ; const before = emTarget . slice ( 0 , range . start + 1 + additionalOffset ) ; const between = emTarget . slice ( range . start + 1 + additionalOffset , range . start + range . length + 1 + additionalOffset ) ; const after = emTarget . slice ( range . start + range . length + 1 + additionalOffset , emTarget . length ) ; emTarget = before + lhs + between + rhs + after ; } ) ; return emTarget ; } }