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 }; 76 77 export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: DiagnosticMessage, includeLib?: boolean) { 78 const t = extractTest(text); 79 const selectionRange = t.ranges.get("selection")!; 80 if (!selectionRange) { 81 throw new Error(`Test ${caption} does not specify selection range`); 82 } 83 84 [Extension.Ts, Extension.Js].forEach(extension => 85 it(`${caption} [${extension}]`, () => runBaseline(extension))); 86 87 function runBaseline(extension: Extension) { 88 const path = "/a" + extension; 89 const { program } = makeProgram({ path, content: t.source }, includeLib); 90 91 if (hasSyntacticDiagnostics(program)) { 92 // Don't bother generating JS baselines for inputs that aren't valid JS. 93 assert.equal(Extension.Js, extension, "Syntactic diagnostics found in non-JS file"); 94 return; 95 } 96 97 const sourceFile = program.getSourceFile(path)!; 98 const context: RefactorContext = { 99 cancellationToken: { throwIfCancellationRequested: noop, isCancellationRequested: returnFalse }, 100 program, 101 file: sourceFile, 102 startPosition: selectionRange.pos, 103 endPosition: selectionRange.end, 104 host: notImplementedHost, 105 formatContext: formatting.getFormatContext(testFormatSettings, notImplementedHost), 106 preferences: emptyOptions, 107 }; 108 const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromRange(selectionRange)); 109 assert.equal(rangeToExtract.errors, undefined, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); 110 const infos = refactor.extractSymbol.getAvailableActions(context); 111 const actions = find(infos, info => info.description === description.message)!.actions; 112 113 const data: string[] = []; 114 data.push(`// ==ORIGINAL==`); 115 data.push(text.replace("[#|", "/*[#|*/").replace("|]", "/*|]*/")); 116 for (const action of actions) { 117 const { renameLocation, edits } = refactor.extractSymbol.getEditsForAction(context, action.name)!; 118 assert.lengthOf(edits, 1); 119 data.push(`// ==SCOPE::${action.description}==`); 120 const newText = textChanges.applyChanges(sourceFile.text, edits[0].textChanges); 121 const newTextWithRename = newText.slice(0, renameLocation) + "/*RENAME*/" + newText.slice(renameLocation); 122 data.push(newTextWithRename); 123 124 const { program: diagProgram } = makeProgram({ path, content: newText }, includeLib); 125 assert.isFalse(hasSyntacticDiagnostics(diagProgram)); 126 } 127 Harness.Baseline.runBaseline(`${baselineFolder}/${caption}${extension}`, data.join(newLineCharacter)); 128 } 129 130 function makeProgram(f: {path: string, content: string }, includeLib?: boolean) { 131 const host = projectSystem.createServerHost(includeLib ? [f, projectSystem.libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required 132 const projectService = projectSystem.createProjectService(host); 133 projectService.openClientFile(f.path); 134 const program = projectService.inferredProjects[0].getLanguageService().getProgram()!; 135 const autoImportProvider = projectService.inferredProjects[0].getLanguageService().getAutoImportProvider(); 136 return { program, autoImportProvider }; 137 } 138 139 function hasSyntacticDiagnostics(program: Program) { 140 const diags = program.getSyntacticDiagnostics(); 141 return length(diags) > 0; 142 } 143 } 144 145 export function testExtractSymbolFailed(caption: string, text: string, description: DiagnosticMessage) { 146 it(caption, () => { 147 const t = extractTest(text); 148 const selectionRange = t.ranges.get("selection"); 149 if (!selectionRange) { 150 throw new Error(`Test ${caption} does not specify selection range`); 151 } 152 const f = { 153 path: "/a.ts", 154 content: t.source 155 }; 156 const host = projectSystem.createServerHost([f, projectSystem.libFile]); 157 const projectService = projectSystem.createProjectService(host); 158 projectService.openClientFile(f.path); 159 const program = projectService.inferredProjects[0].getLanguageService().getProgram()!; 160 const sourceFile = program.getSourceFile(f.path)!; 161 const context: RefactorContext = { 162 cancellationToken: { throwIfCancellationRequested: noop, isCancellationRequested: returnFalse }, 163 program, 164 file: sourceFile, 165 startPosition: selectionRange.pos, 166 endPosition: selectionRange.end, 167 host: notImplementedHost, 168 formatContext: formatting.getFormatContext(testFormatSettings, notImplementedHost), 169 preferences: emptyOptions, 170 }; 171 const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromRange(selectionRange)); 172 assert.isUndefined(rangeToExtract.errors, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); 173 const infos = refactor.extractSymbol.getAvailableActions(context); 174 assert.isUndefined(find(infos, info => info.description === description.message)); 175 }); 176 } 177} 178