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