1namespace ts { 2 interface Range { 3 pos: number; 4 end: number; 5 name: string; 6 } 7 8 interface Test { 9 source: string; 10 ranges: ESMap<string, Range>; 11 } 12 13 export function extractTest(source: string): Test { 14 const activeRanges: Range[] = []; 15 let text = ""; 16 let lastPos = 0; 17 let pos = 0; 18 const ranges = new Map<string, Range>(); 19 20 while (pos < source.length) { 21 if (source.charCodeAt(pos) === CharacterCodes.openBracket && 22 (source.charCodeAt(pos + 1) === CharacterCodes.hash || source.charCodeAt(pos + 1) === CharacterCodes.$)) { 23 const saved = pos; 24 pos += 2; 25 const s = pos; 26 consumeIdentifier(); 27 const e = pos; 28 if (source.charCodeAt(pos) === CharacterCodes.bar) { 29 pos++; 30 text += source.substring(lastPos, saved); 31 const name = s === e 32 ? source.charCodeAt(saved + 1) === CharacterCodes.hash ? "selection" : "extracted" 33 : source.substring(s, e); 34 activeRanges.push({ name, pos: text.length, end: undefined! }); // TODO: GH#18217 35 lastPos = pos; 36 continue; 37 } 38 else { 39 pos = saved; 40 } 41 } 42 else if (source.charCodeAt(pos) === CharacterCodes.bar && source.charCodeAt(pos + 1) === CharacterCodes.closeBracket) { 43 text += source.substring(lastPos, pos); 44 activeRanges[activeRanges.length - 1].end = text.length; 45 const range = activeRanges.pop()!; 46 if (hasProperty(ranges, range.name)) { 47 throw new Error(`Duplicate name of range ${range.name}`); 48 } 49 ranges.set(range.name, range); 50 pos += 2; 51 lastPos = pos; 52 continue; 53 } 54 pos++; 55 } 56 text += source.substring(lastPos, pos); 57 58 function consumeIdentifier() { 59 while (isIdentifierPart(source.charCodeAt(pos), ScriptTarget.Latest)) { 60 pos++; 61 } 62 } 63 return { source: text, ranges }; 64 } 65 66 export const newLineCharacter = "\n"; 67 68 export const notImplementedHost: LanguageServiceHost = { 69 getCompilationSettings: notImplemented, 70 getScriptFileNames: notImplemented, 71 getScriptVersion: notImplemented, 72 getScriptSnapshot: notImplemented, 73 getDefaultLibFileName: notImplemented, 74 getCurrentDirectory: notImplemented, 75 readFile: notImplemented, 76 fileExists: notImplemented 77 }; 78 79 export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: DiagnosticMessage, includeLib?: boolean) { 80 const t = extractTest(text); 81 const selectionRange = t.ranges.get("selection")!; 82 if (!selectionRange) { 83 throw new Error(`Test ${caption} does not specify selection range`); 84 } 85 86 [Extension.Ts, Extension.Js].forEach(extension => 87 it(`${caption} [${extension}]`, () => runBaseline(extension))); 88 89 function runBaseline(extension: Extension) { 90 const path = "/a" + extension; 91 const { program } = makeProgram({ path, content: t.source }, includeLib); 92 93 if (hasSyntacticDiagnostics(program)) { 94 // Don't bother generating JS baselines for inputs that aren't valid JS. 95 assert.equal(Extension.Js, extension, "Syntactic diagnostics found in non-JS file"); 96 return; 97 } 98 99 const sourceFile = program.getSourceFile(path)!; 100 const context: RefactorContext = { 101 cancellationToken: { throwIfCancellationRequested: noop, isCancellationRequested: returnFalse }, 102 program, 103 file: sourceFile, 104 startPosition: selectionRange.pos, 105 endPosition: selectionRange.end, 106 host: notImplementedHost, 107 formatContext: formatting.getFormatContext(testFormatSettings, notImplementedHost), 108 preferences: emptyOptions, 109 }; 110 const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromRange(selectionRange)); 111 assert.equal(rangeToExtract.errors, undefined, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); 112 const infos = refactor.extractSymbol.getRefactorActionsToExtractSymbol(context); 113 const actions = find(infos, info => info.description === description.message)!.actions; 114 115 const data: string[] = []; 116 data.push(`// ==ORIGINAL==`); 117 data.push(text.replace("[#|", "/*[#|*/").replace("|]", "/*|]*/")); 118 for (const action of actions) { 119 const { renameLocation, edits } = refactor.extractSymbol.getRefactorEditsToExtractSymbol(context, action.name)!; 120 assert.lengthOf(edits, 1); 121 data.push(`// ==SCOPE::${action.description}==`); 122 const newText = textChanges.applyChanges(sourceFile.text, edits[0].textChanges); 123 const newTextWithRename = newText.slice(0, renameLocation) + "/*RENAME*/" + newText.slice(renameLocation); 124 data.push(newTextWithRename); 125 126 const { program: diagProgram } = makeProgram({ path, content: newText }, includeLib); 127 assert.isFalse(hasSyntacticDiagnostics(diagProgram)); 128 } 129 Harness.Baseline.runBaseline(`${baselineFolder}/${caption}${extension}`, data.join(newLineCharacter)); 130 } 131 132 function makeProgram(f: {path: string, content: string }, includeLib?: boolean) { 133 const host = projectSystem.createServerHost(includeLib ? [f, projectSystem.libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required 134 const projectService = projectSystem.createProjectService(host); 135 projectService.openClientFile(f.path); 136 const program = projectService.inferredProjects[0].getLanguageService().getProgram()!; 137 const autoImportProvider = projectService.inferredProjects[0].getLanguageService().getAutoImportProvider(); 138 return { program, autoImportProvider }; 139 } 140 141 function hasSyntacticDiagnostics(program: Program) { 142 const diags = program.getSyntacticDiagnostics(); 143 return length(diags) > 0; 144 } 145 } 146 147 export function testExtractSymbolFailed(caption: string, text: string, description: DiagnosticMessage) { 148 it(caption, () => { 149 const t = extractTest(text); 150 const selectionRange = t.ranges.get("selection"); 151 if (!selectionRange) { 152 throw new Error(`Test ${caption} does not specify selection range`); 153 } 154 const f = { 155 path: "/a.ts", 156 content: t.source 157 }; 158 const host = projectSystem.createServerHost([f, projectSystem.libFile]); 159 const projectService = projectSystem.createProjectService(host); 160 projectService.openClientFile(f.path); 161 const program = projectService.inferredProjects[0].getLanguageService().getProgram()!; 162 const sourceFile = program.getSourceFile(f.path)!; 163 const context: RefactorContext = { 164 cancellationToken: { throwIfCancellationRequested: noop, isCancellationRequested: returnFalse }, 165 program, 166 file: sourceFile, 167 startPosition: selectionRange.pos, 168 endPosition: selectionRange.end, 169 host: notImplementedHost, 170 formatContext: formatting.getFormatContext(testFormatSettings, notImplementedHost), 171 preferences: emptyOptions, 172 }; 173 const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromRange(selectionRange)); 174 assert.isUndefined(rangeToExtract.errors, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); 175 const infos = refactor.extractSymbol.getRefactorActionsToExtractSymbol(context); 176 assert.isUndefined(find(infos, info => info.description === description.message)); 177 }); 178 } 179} 180