diff --git a/src/harness/unittests/extractConstants.ts b/src/harness/unittests/extractConstants.ts index 61ffbd01a639b..8c1d835ea2758 100644 --- a/src/harness/unittests/extractConstants.ts +++ b/src/harness/unittests/extractConstants.ts @@ -264,11 +264,13 @@ namespace N { // Force this test to be TS-only }`); }); - function testExtractConstant(caption: string, text: string) { - testExtractSymbol(caption, text, "extractConstant", Diagnostics.Extract_constant); + function testExtractConstant(caption: string, text: string | { [index: string]: string }) { + if (typeof text === "string") text = { "a": text }; + testExtractSymbol(caption, createMapFromTemplate(text), "extractConstant", Diagnostics.Extract_constant); } - function testExtractConstantFailed(caption: string, text: string) { - testExtractSymbolFailed(caption, text, Diagnostics.Extract_constant); + function testExtractConstantFailed(caption: string, text: string | { [index: string]: string }) { + if (typeof text === "string") text = { "a": text }; + testExtractSymbolFailed(caption, createMapFromTemplate(text), Diagnostics.Extract_constant); } } diff --git a/src/harness/unittests/extractFunctions.ts b/src/harness/unittests/extractFunctions.ts index 56b3c35a2920d..77a159ba83445 100644 --- a/src/harness/unittests/extractFunctions.ts +++ b/src/harness/unittests/extractFunctions.ts @@ -546,9 +546,30 @@ var q = /*b*/ //c /*g*/ + /*h*/ //i /*j*/ 2|] /*k*/ //l /*m*/; /*n*/ //o`); + + // https://github.com/Microsoft/TypeScript/issues/19328 + testExtractFunction("extractFunction_importedGenericType", { + "someTypes": ` +export type Dictionary = { + [key: string]: T; + }; + +export type A = string | string[]; + +export type B = Dictionary;`, + "index": ` +import { A, B, Dictionary} from './someTypes'; + +function doThing(): void { + const aBunchOfA: Dictionary = {}; + + [#|aBunchOfA['key'] = { a: 'thing' };|] +}` + }); }); - function testExtractFunction(caption: string, text: string, includeLib?: boolean) { - testExtractSymbol(caption, text, "extractFunction", Diagnostics.Extract_function, includeLib); + function testExtractFunction(caption: string, text: string | { [index: string]: string }, includeLib?: boolean) { + if (typeof text === "string") text = { "a": text }; + testExtractSymbol(caption, createMapFromTemplate(text), "extractFunction", Diagnostics.Extract_function, includeLib); } } diff --git a/src/harness/unittests/extractRanges.ts b/src/harness/unittests/extractRanges.ts index e8e9a918f0dde..7d3ca0aa57d08 100644 --- a/src/harness/unittests/extractRanges.ts +++ b/src/harness/unittests/extractRanges.ts @@ -3,9 +3,10 @@ namespace ts { function testExtractRangeFailed(caption: string, s: string, expectedErrors: string[]) { return it(caption, () => { - const t = extractTest(s); - const file = createSourceFile("a.ts", t.source, ScriptTarget.Latest, /*setParentNodes*/ true); - const selectionRange = t.ranges.get("selection"); + const t = extractTest(createMapFromTemplate({ "a": s })); + const files = arrayFrom(t.files.values()); + const file = createSourceFile("a.ts", files[0].source, ScriptTarget.Latest, /*setParentNodes*/ true); + const selectionRange = files[0].ranges.get("selection"); if (!selectionRange) { throw new Error(`Test ${s} does not specify selection range`); } @@ -17,14 +18,15 @@ namespace ts { } function testExtractRange(s: string): void { - const t = extractTest(s); - const f = createSourceFile("a.ts", t.source, ScriptTarget.Latest, /*setParentNodes*/ true); - const selectionRange = t.ranges.get("selection"); + const t = extractTest(createMapFromTemplate({ "a": s })); + const files = arrayFrom(t.files.values()); + const f = createSourceFile("a.ts", files[0].source, ScriptTarget.Latest, /*setParentNodes*/ true); + const selectionRange = files[0].ranges.get("selection"); if (!selectionRange) { throw new Error(`Test ${s} does not specify selection range`); } const result = refactor.extractSymbol.getRangeToExtract(f, createTextSpanFromBounds(selectionRange.start, selectionRange.end)); - const expectedRange = t.ranges.get("extracted"); + const expectedRange = files[0].ranges.get("extracted"); if (expectedRange) { let start: number, end: number; if (ts.isArray(result.targetRange.range)) { diff --git a/src/harness/unittests/extractTestHelpers.ts b/src/harness/unittests/extractTestHelpers.ts index b8974694cfed4..7e243a689bf4a 100644 --- a/src/harness/unittests/extractTestHelpers.ts +++ b/src/harness/unittests/extractTestHelpers.ts @@ -9,61 +9,68 @@ namespace ts { } export interface Test { - source: string; - ranges: Map; + files: Map<{ + source: string; + ranges: Map; + }>; } - export function extractTest(source: string): Test { - const activeRanges: Range[] = []; - let text = ""; - let lastPos = 0; - let pos = 0; - const ranges = createMap(); - - while (pos < source.length) { - if (source.charCodeAt(pos) === CharacterCodes.openBracket && - (source.charCodeAt(pos + 1) === CharacterCodes.hash || source.charCodeAt(pos + 1) === CharacterCodes.$)) { - const saved = pos; - pos += 2; - const s = pos; - consumeIdentifier(); - const e = pos; - if (source.charCodeAt(pos) === CharacterCodes.bar) { + export function extractTest(sources: Map): Test { + const files: Test["files"] = createMap(); + const entries = sources.entries(); + for (let {done, value} = entries.next(); !done; { done, value } = entries.next()) { + const [ filename, source ] = value; + const activeRanges: Range[] = []; + let text = ""; + let lastPos = 0; + let pos = 0; + const ranges = createMap(); + const consumeIdentifier = () => { + while (isIdentifierPart(source.charCodeAt(pos), ScriptTarget.Latest)) { pos++; - text += source.substring(lastPos, saved); - const name = s === e - ? source.charCodeAt(saved + 1) === CharacterCodes.hash ? "selection" : "extracted" - : source.substring(s, e); - activeRanges.push({ name, start: text.length, end: undefined }); - lastPos = pos; - continue; } - else { - pos = saved; + }; + + while (pos < source.length) { + if (source.charCodeAt(pos) === CharacterCodes.openBracket && + (source.charCodeAt(pos + 1) === CharacterCodes.hash || source.charCodeAt(pos + 1) === CharacterCodes.$)) { + const saved = pos; + pos += 2; + const s = pos; + consumeIdentifier(); + const e = pos; + if (source.charCodeAt(pos) === CharacterCodes.bar) { + pos++; + text += source.substring(lastPos, saved); + const name = s === e + ? source.charCodeAt(saved + 1) === CharacterCodes.hash ? "selection" : "extracted" + : source.substring(s, e); + activeRanges.push({ name, start: text.length, end: undefined }); + lastPos = pos; + continue; + } + else { + pos = saved; + } } - } - else if (source.charCodeAt(pos) === CharacterCodes.bar && source.charCodeAt(pos + 1) === CharacterCodes.closeBracket) { - text += source.substring(lastPos, pos); - activeRanges[activeRanges.length - 1].end = text.length; - const range = activeRanges.pop(); - if (range.name in ranges) { - throw new Error(`Duplicate name of range ${range.name}`); + else if (source.charCodeAt(pos) === CharacterCodes.bar && source.charCodeAt(pos + 1) === CharacterCodes.closeBracket) { + text += source.substring(lastPos, pos); + activeRanges[activeRanges.length - 1].end = text.length; + const range = activeRanges.pop(); + if (range.name in ranges) { + throw new Error(`Duplicate name of range ${range.name}`); + } + ranges.set(range.name, range); + pos += 2; + lastPos = pos; + continue; } - ranges.set(range.name, range); - pos += 2; - lastPos = pos; - continue; - } - pos++; - } - text += source.substring(lastPos, pos); - - function consumeIdentifier() { - while (isIdentifierPart(source.charCodeAt(pos), ScriptTarget.Latest)) { pos++; } + text += source.substring(lastPos, pos); + files.set(filename, { source: text, ranges }); } - return { source: text, ranges }; + return { files }; } export const newLineCharacter = "\n"; @@ -104,19 +111,22 @@ namespace ts { getCurrentDirectory: notImplemented, }; - export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: DiagnosticMessage, includeLib?: boolean) { - const t = extractTest(text); - const selectionRange = t.ranges.get("selection"); - if (!selectionRange) { + export function testExtractSymbol(caption: string, files: Map, baselineFolder: string, description: DiagnosticMessage, includeLib?: boolean) { + const t = extractTest(files); + const fsentries = arrayFrom(t.files.entries()); + const selectionRangeCandidates = filter(fsentries, t => !!t[1].ranges.get("selection")); + if (!selectionRangeCandidates || !selectionRangeCandidates.length) { throw new Error(`Test ${caption} does not specify selection range`); } + const selectionRange = selectionRangeCandidates[0][1].ranges.get("selection"); [Extension.Ts, Extension.Js].forEach(extension => it(`${caption} [${extension}]`, () => runBaseline(extension))); function runBaseline(extension: Extension) { - const path = "/a" + extension; - const program = makeProgram({ path, content: t.source }, includeLib); + const path = `/${selectionRangeCandidates[0][0]}${extension}`; + const text = files.get(selectionRangeCandidates[0][0]); + const program = makeProgram(map(fsentries, ([name, t]) => ({ path: `/${name}${extension}`, content: t.source })), includeLib); if (hasSyntacticDiagnostics(program)) { // Don't bother generating JS baselines for inputs that aren't valid JS. @@ -152,17 +162,17 @@ namespace ts { const newTextWithRename = newText.slice(0, renameLocation) + "/*RENAME*/" + newText.slice(renameLocation); data.push(newTextWithRename); - const diagProgram = makeProgram({ path, content: newText }, includeLib); + const diagProgram = makeProgram(map(fsentries, ([name, t]) => (`/${name}${extension}` === path ? { path, content: newText } : { path: `/${name}${extension}`, content: t.source })), includeLib); assert.isFalse(hasSyntacticDiagnostics(diagProgram)); } return data.join(newLineCharacter); }); } - function makeProgram(f: {path: string, content: string }, includeLib?: boolean) { - const host = projectSystem.createServerHost(includeLib ? [f, projectSystem.libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required + function makeProgram(f: {path: string, content: string }[], includeLib?: boolean) { + const host = projectSystem.createServerHost(includeLib ? [...f, projectSystem.libFile] : f); // libFile is expensive to parse repeatedly - only test when required const projectService = projectSystem.createProjectService(host); - projectService.openClientFile(f.path); + projectService.openClientFile(f[f.length - 1].path); const program = projectService.inferredProjects[0].getLanguageService().getProgram(); return program; } @@ -173,22 +183,22 @@ namespace ts { } } - export function testExtractSymbolFailed(caption: string, text: string, description: DiagnosticMessage) { + export function testExtractSymbolFailed(caption: string, files: Map, description: DiagnosticMessage) { it(caption, () => { - const t = extractTest(text); - const selectionRange = t.ranges.get("selection"); - if (!selectionRange) { + const t = extractTest(files); + const fsentries = arrayFrom(t.files.entries()); + const selectionRangeCandidates = filter(fsentries, t => !!t[1].ranges.get("selection")); + if (!selectionRangeCandidates || !selectionRangeCandidates.length) { throw new Error(`Test ${caption} does not specify selection range`); } - const f = { - path: "/a.ts", - content: t.source - }; - const host = projectSystem.createServerHost([f, projectSystem.libFile]); + const selectionRange = selectionRangeCandidates[0][1].ranges.get("selection"); + const testPath = `/${selectionRangeCandidates[0][0]}.ts`; + + const host = projectSystem.createServerHost([...map(fsentries, ([name, t]) => ({ path: `/${name}.ts`, content: t.source })), projectSystem.libFile]); const projectService = projectSystem.createProjectService(host); - projectService.openClientFile(f.path); + projectService.openClientFile(testPath); const program = projectService.inferredProjects[0].getLanguageService().getProgram(); - const sourceFile = program.getSourceFile(f.path); + const sourceFile = program.getSourceFile(testPath); const context: RefactorContext = { cancellationToken: { throwIfCancellationRequested() { }, isCancellationRequested() { return false; } }, newLineCharacter, diff --git a/tests/baselines/reference/extractFunction/extractFunction_importedGenericType.ts b/tests/baselines/reference/extractFunction/extractFunction_importedGenericType.ts new file mode 100644 index 0000000000000..714a855b4f63c --- /dev/null +++ b/tests/baselines/reference/extractFunction/extractFunction_importedGenericType.ts @@ -0,0 +1,35 @@ +// ==ORIGINAL== + +import { A, B, Dictionary} from './someTypes'; + +function doThing(): void { + const aBunchOfA: Dictionary = {}; + + /*[#|*/aBunchOfA['key'] = { a: 'thing' };/*|]*/ +} +// ==SCOPE::Extract to inner function in function 'doThing'== + +import { A, B, Dictionary} from './someTypes'; + +function doThing(): void { + const aBunchOfA: Dictionary = {}; + + /*RENAME*/newFunction(); + + function newFunction() { + aBunchOfA['key'] = { a: 'thing' }; + } +} +// ==SCOPE::Extract to function in module scope== + +import { A, B, Dictionary} from './someTypes'; + +function doThing(): void { + const aBunchOfA: Dictionary = {}; + + /*RENAME*/newFunction(aBunchOfA); +} + +function newFunction(aBunchOfA: Dictionary>) { + aBunchOfA['key'] = { a: 'thing' }; +}