1import * as ts from "../_namespaces/ts"; 2 3const enum ChangedPart { 4 none = 0, 5 references = 1 << 0, 6 importsAndExports = 1 << 1, 7 program = 1 << 2 8} 9 10export const newLine = "\r\n"; 11 12export interface SourceFileWithText extends ts.SourceFile { 13 sourceText?: SourceText; 14} 15 16export interface NamedSourceText { 17 name: string; 18 text: SourceText; 19} 20 21export interface ProgramWithSourceTexts extends ts.Program { 22 sourceTexts?: readonly NamedSourceText[]; 23 host: TestCompilerHost; 24} 25 26export interface TestCompilerHost extends ts.CompilerHost { 27 getTrace(): string[]; 28} 29 30export class SourceText implements ts.IScriptSnapshot { 31 private fullText: string | undefined; 32 33 constructor(private references: string, 34 private importsAndExports: string, 35 private program: string, 36 private changedPart: ChangedPart = 0, 37 private version = 0) { 38 } 39 40 static New(references: string, importsAndExports: string, program: string): SourceText { 41 ts.Debug.assert(references !== undefined); 42 ts.Debug.assert(importsAndExports !== undefined); 43 ts.Debug.assert(program !== undefined); 44 return new SourceText(references + newLine, importsAndExports + newLine, program || ""); 45 } 46 47 public getVersion(): number { 48 return this.version; 49 } 50 51 public updateReferences(newReferences: string): SourceText { 52 ts.Debug.assert(newReferences !== undefined); 53 return new SourceText(newReferences + newLine, this.importsAndExports, this.program, this.changedPart | ChangedPart.references, this.version + 1); 54 } 55 public updateImportsAndExports(newImportsAndExports: string): SourceText { 56 ts.Debug.assert(newImportsAndExports !== undefined); 57 return new SourceText(this.references, newImportsAndExports + newLine, this.program, this.changedPart | ChangedPart.importsAndExports, this.version + 1); 58 } 59 public updateProgram(newProgram: string): SourceText { 60 ts.Debug.assert(newProgram !== undefined); 61 return new SourceText(this.references, this.importsAndExports, newProgram, this.changedPart | ChangedPart.program, this.version + 1); 62 } 63 64 public getFullText() { 65 return this.fullText || (this.fullText = this.references + this.importsAndExports + this.program); 66 } 67 68 public getText(start: number, end: number): string { 69 return this.getFullText().substring(start, end); 70 } 71 72 getLength(): number { 73 return this.getFullText().length; 74 } 75 76 getChangeRange(oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange { 77 const oldText = oldSnapshot as SourceText; 78 let oldSpan: ts.TextSpan; 79 let newLength: number; 80 switch (oldText.changedPart ^ this.changedPart) { 81 case ChangedPart.references: 82 oldSpan = ts.createTextSpan(0, oldText.references.length); 83 newLength = this.references.length; 84 break; 85 case ChangedPart.importsAndExports: 86 oldSpan = ts.createTextSpan(oldText.references.length, oldText.importsAndExports.length); 87 newLength = this.importsAndExports.length; 88 break; 89 case ChangedPart.program: 90 oldSpan = ts.createTextSpan(oldText.references.length + oldText.importsAndExports.length, oldText.program.length); 91 newLength = this.program.length; 92 break; 93 default: 94 return ts.Debug.fail("Unexpected change"); 95 } 96 97 return ts.createTextChangeRange(oldSpan, newLength); 98 } 99} 100 101function createSourceFileWithText(fileName: string, sourceText: SourceText, target: ts.ScriptTarget) { 102 const file = ts.createSourceFile(fileName, sourceText.getFullText(), target) as SourceFileWithText; 103 file.sourceText = sourceText; 104 file.version = "" + sourceText.getVersion(); 105 return file; 106} 107 108export function createTestCompilerHost(texts: readonly NamedSourceText[], target: ts.ScriptTarget, oldProgram?: ProgramWithSourceTexts, useGetSourceFileByPath?: boolean) { 109 const files = ts.arrayToMap(texts, t => t.name, t => { 110 if (oldProgram) { 111 let oldFile = oldProgram.getSourceFile(t.name) as SourceFileWithText; 112 if (oldFile && oldFile.redirectInfo) { 113 oldFile = oldFile.redirectInfo.unredirected; 114 } 115 if (oldFile && oldFile.sourceText!.getVersion() === t.text.getVersion()) { 116 return oldFile; 117 } 118 } 119 return createSourceFileWithText(t.name, t.text, target); 120 }); 121 const useCaseSensitiveFileNames = ts.sys && ts.sys.useCaseSensitiveFileNames; 122 const getCanonicalFileName = ts.createGetCanonicalFileName(useCaseSensitiveFileNames); 123 const trace: string[] = []; 124 const result: TestCompilerHost = { 125 trace: s => trace.push(s), 126 getTrace: () => trace, 127 getSourceFile: fileName => files.get(fileName), 128 getDefaultLibFileName: () => "lib.d.ts", 129 writeFile: ts.notImplemented, 130 getCurrentDirectory: () => "", 131 getDirectories: () => [], 132 getCanonicalFileName, 133 useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, 134 getNewLine: () => ts.sys ? ts.sys.newLine : newLine, 135 fileExists: fileName => files.has(fileName), 136 readFile: fileName => { 137 const file = files.get(fileName); 138 return file && file.text; 139 }, 140 }; 141 if (useGetSourceFileByPath) { 142 const filesByPath = ts.mapEntries(files, (fileName, file) => [ts.toPath(fileName, "", getCanonicalFileName), file]); 143 result.getSourceFileByPath = (_fileName, path) => filesByPath.get(path); 144 } 145 return result; 146} 147 148export function newProgram(texts: NamedSourceText[], rootNames: string[], options: ts.CompilerOptions, useGetSourceFileByPath?: boolean): ProgramWithSourceTexts { 149 const host = createTestCompilerHost(texts, options.target!, /*oldProgram*/ undefined, useGetSourceFileByPath); 150 const program = ts.createProgram(rootNames, options, host) as ProgramWithSourceTexts; 151 program.sourceTexts = texts; 152 program.host = host; 153 return program; 154} 155 156export function updateProgram(oldProgram: ProgramWithSourceTexts, rootNames: readonly string[], options: ts.CompilerOptions, updater: (files: NamedSourceText[]) => void, newTexts?: NamedSourceText[], useGetSourceFileByPath?: boolean) { 157 if (!newTexts) { 158 newTexts = oldProgram.sourceTexts!.slice(0); 159 } 160 updater(newTexts); 161 const host = createTestCompilerHost(newTexts, options.target!, oldProgram, useGetSourceFileByPath); 162 const program = ts.createProgram(rootNames, options, host, oldProgram) as ProgramWithSourceTexts; 163 program.sourceTexts = newTexts; 164 program.host = host; 165 return program; 166} 167 168export function updateProgramText(files: readonly NamedSourceText[], fileName: string, newProgramText: string) { 169 const file = ts.find(files, f => f.name === fileName)!; 170 file.text = file.text.updateProgram(newProgramText); 171} 172 173export function checkResolvedTypeDirective(actual: ts.ResolvedTypeReferenceDirective, expected: ts.ResolvedTypeReferenceDirective) { 174 assert.equal(actual.resolvedFileName, expected.resolvedFileName, `'resolvedFileName': expected '${actual.resolvedFileName}' to be equal to '${expected.resolvedFileName}'`); 175 assert.equal(actual.primary, expected.primary, `'primary': expected '${actual.primary}' to be equal to '${expected.primary}'`); 176 return true; 177} 178 179function checkCache<T>(caption: string, program: ts.Program, fileName: string, expectedContent: ts.ESMap<string, T> | undefined, getCache: (f: ts.SourceFile) => ts.ModeAwareCache<T> | undefined, entryChecker: (expected: T, original: T) => boolean): void { 180 const file = program.getSourceFile(fileName); 181 assert.isTrue(file !== undefined, `cannot find file ${fileName}`); 182 const cache = getCache(file!); 183 if (expectedContent === undefined) { 184 assert.isTrue(cache === undefined, `expected ${caption} to be undefined`); 185 } 186 else { 187 assert.isTrue(cache !== undefined, `expected ${caption} to be set`); 188 assert.isTrue(mapEqualToCache(expectedContent, cache!, entryChecker), `contents of ${caption} did not match the expected contents.`); 189 } 190} 191 192/** True if the maps have the same keys and values. */ 193function mapEqualToCache<T>(left: ts.ESMap<string, T>, right: ts.ModeAwareCache<T>, valuesAreEqual?: (left: T, right: T) => boolean): boolean { 194 // given the type mismatch (the tests never pass a cache), this'll never be true 195 if (left as any === right) { 196 return true; 197 } 198 if (!left || !right) { 199 return false; 200 } 201 const someInLeftHasNoMatch = ts.forEachEntry(left, (leftValue, leftKey) => { 202 if (!right.has(leftKey, /*mode*/ undefined)) { 203 return true; 204 } 205 const rightValue = right.get(leftKey, /*mode*/ undefined)!; 206 return !(valuesAreEqual ? valuesAreEqual(leftValue, rightValue) : leftValue === rightValue); 207 }); 208 if (someInLeftHasNoMatch) { 209 return false; 210 } 211 let someInRightHasNoMatch = false; 212 right.forEach((_, rightKey) => someInRightHasNoMatch = someInRightHasNoMatch || !left.has(rightKey)); 213 return !someInRightHasNoMatch; 214} 215 216export function checkResolvedModulesCache(program: ts.Program, fileName: string, expectedContent: ts.ESMap<string, ts.ResolvedModule | undefined> | undefined): void { 217 checkCache("resolved modules", program, fileName, expectedContent, f => f.resolvedModules, ts.checkResolvedModule); 218} 219 220export function checkResolvedTypeDirectivesCache(program: ts.Program, fileName: string, expectedContent: ts.ESMap<string, ts.ResolvedTypeReferenceDirective> | undefined): void { 221 checkCache("resolved type directives", program, fileName, expectedContent, f => f.resolvedTypeReferenceDirectiveNames, checkResolvedTypeDirective); 222} 223 224export function createResolvedModule(resolvedFileName: string, isExternalLibraryImport = false): ts.ResolvedModuleFull { 225 return { resolvedFileName, extension: ts.extensionFromPath(resolvedFileName), isExternalLibraryImport }; 226} 227 228export function checkResolvedModule(actual: ts.ResolvedModuleFull | undefined, expected: ts.ResolvedModuleFull | undefined): boolean { 229 if (!expected) { 230 if (actual) { 231 assert.fail(actual, expected, "expected resolved module to be undefined"); 232 return false; 233 } 234 return true; 235 } 236 else if (!actual) { 237 assert.fail(actual, expected, "expected resolved module to be defined"); 238 return false; 239 } 240 241 assert.isTrue(actual.resolvedFileName === expected.resolvedFileName, `'resolvedFileName': expected '${actual.resolvedFileName}' to be equal to '${expected.resolvedFileName}'`); 242 assert.isTrue(actual.extension === expected.extension, `'ext': expected '${actual.extension}' to be equal to '${expected.extension}'`); 243 assert.isTrue(actual.isExternalLibraryImport === expected.isExternalLibraryImport, `'isExternalLibraryImport': expected '${actual.isExternalLibraryImport}' to be equal to '${expected.isExternalLibraryImport}'`); 244 return true; 245} 246 247export function checkResolvedModuleWithFailedLookupLocations(actual: ts.ResolvedModuleWithFailedLookupLocations, expectedResolvedModule: ts.ResolvedModuleFull, expectedFailedLookupLocations: string[]): void { 248 assert.isTrue(actual.resolvedModule !== undefined, "module should be resolved"); 249 checkResolvedModule(actual.resolvedModule, expectedResolvedModule); 250 assert.deepEqual(actual.failedLookupLocations, expectedFailedLookupLocations, `Failed lookup locations should match - expected has ${expectedFailedLookupLocations.length}, actual has ${actual.failedLookupLocations.length}`); 251}