diff --git a/Jakefile.js b/Jakefile.js index 359549d8248..8a4c67ac84b 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -140,6 +140,8 @@ var harnessSources = harnessCoreSources.concat([ "initializeTSConfig.ts", "extractConstants.ts", "extractFunctions.ts", + "extractRanges.ts", + "extractTestHelpers.ts", "printer.ts", "textChanges.ts", "telemetry.ts", diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index cb2cb00b861..88999b2d979 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -130,6 +130,8 @@ "./unittests/customTransforms.ts", "./unittests/extractConstants.ts", "./unittests/extractFunctions.ts", + "./unittests/extractRanges.ts", + "./unittests/extractTestHelpers.ts", "./unittests/textChanges.ts", "./unittests/telemetry.ts", "./unittests/languageService.ts", diff --git a/src/harness/unittests/extractConstants.ts b/src/harness/unittests/extractConstants.ts index f5018811b1a..fb776eb6af3 100644 --- a/src/harness/unittests/extractConstants.ts +++ b/src/harness/unittests/extractConstants.ts @@ -1,104 +1,6 @@ -/// -/// +/// namespace ts { - interface Range { - start: number; - end: number; - name: string; - } - - interface Test { - source: string; - ranges: Map; - } - - // TODO (acasey): share - 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) { - 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}`); - } - 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++; - } - } - return { source: text, ranges }; - } - - // TODO (acasey): share - const newLineCharacter = "\n"; - function getRuleProvider(action?: (opts: FormatCodeSettings) => void) { - const options = { - indentSize: 4, - tabSize: 4, - newLineCharacter, - convertTabsToSpaces: true, - indentStyle: ts.IndentStyle.Smart, - insertSpaceAfterConstructor: false, - insertSpaceAfterCommaDelimiter: true, - insertSpaceAfterSemicolonInForStatements: true, - insertSpaceBeforeAndAfterBinaryOperators: true, - insertSpaceAfterKeywordsInControlFlowStatements: true, - insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, - insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, - insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false, - insertSpaceBeforeFunctionParenthesis: false, - placeOpenBraceOnNewLineForFunctions: false, - placeOpenBraceOnNewLineForControlBlocks: false, - }; - if (action) { - action(options); - } - const rulesProvider = new formatting.RulesProvider(); - rulesProvider.ensureUpToDate(options); - return rulesProvider; - } - describe("extractConstants", () => { testExtractConstant("extractConstant_TopLevel", `let x = [#|1|];`); @@ -168,83 +70,11 @@ namespace ts { }`); }); - // TODO (acasey): share? function testExtractConstant(caption: string, text: string) { - it(caption, () => { - Harness.Baseline.runBaseline(`extractConstant/${caption}.ts`, () => { - const t = extractTest(text); - const selectionRange = t.ranges.get("selection"); - if (!selectionRange) { - 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 projectService = projectSystem.createProjectService(host); - projectService.openClientFile(f.path); - const program = projectService.inferredProjects[0].getLanguageService().getProgram(); - const sourceFile = program.getSourceFile(f.path); - const context: RefactorContext = { - cancellationToken: { throwIfCancellationRequested() { }, isCancellationRequested() { return false; } }, - newLineCharacter, - program, - file: sourceFile, - startPosition: selectionRange.start, - endPosition: selectionRange.end, - rulesProvider: getRuleProvider() - }; - const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromBounds(selectionRange.start, selectionRange.end)); - assert.equal(rangeToExtract.errors, undefined, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); - const infos = refactor.extractSymbol.getAvailableActions(context); - const actions = find(infos, info => info.description === Diagnostics.Extract_constant.message).actions; - const data: string[] = []; - data.push(`// ==ORIGINAL==`); - data.push(sourceFile.text); - for (const action of actions) { - const { renameLocation, edits } = refactor.extractSymbol.getEditsForAction(context, action.name); - assert.lengthOf(edits, 1); - data.push(`// ==SCOPE::${action.description}==`); - const newText = textChanges.applyChanges(sourceFile.text, edits[0].textChanges); - const newTextWithRename = newText.slice(0, renameLocation) + "/*RENAME*/" + newText.slice(renameLocation); - data.push(newTextWithRename); - } - return data.join(newLineCharacter); - }); - }); + testExtractSymbol(caption, text, "extractConstant", Diagnostics.Extract_constant); } - // TODO (acasey): share? function testExtractConstantFailed(caption: string, text: string) { - it(caption, () => { - const t = extractTest(text); - const selectionRange = t.ranges.get("selection"); - if (!selectionRange) { - 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 projectService = projectSystem.createProjectService(host); - projectService.openClientFile(f.path); - const program = projectService.inferredProjects[0].getLanguageService().getProgram(); - const sourceFile = program.getSourceFile(f.path); - const context: RefactorContext = { - cancellationToken: { throwIfCancellationRequested() { }, isCancellationRequested() { return false; } }, - newLineCharacter, - program, - file: sourceFile, - startPosition: selectionRange.start, - endPosition: selectionRange.end, - rulesProvider: getRuleProvider() - }; - const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromBounds(selectionRange.start, selectionRange.end)); - assert.isUndefined(rangeToExtract.errors, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); - const infos = refactor.extractSymbol.getAvailableActions(context); - assert.isUndefined(find(infos, info => info.description === Diagnostics.Extract_constant.message)); - }); + testExtractSymbolFailed(caption, text, Diagnostics.Extract_constant); } } diff --git a/src/harness/unittests/extractFunctions.ts b/src/harness/unittests/extractFunctions.ts index be5d21a80ae..2497b2eec51 100644 --- a/src/harness/unittests/extractFunctions.ts +++ b/src/harness/unittests/extractFunctions.ts @@ -1,417 +1,7 @@ -/// -/// +/// namespace ts { - interface Range { - start: number; - end: number; - name: string; - } - - interface Test { - source: string; - ranges: Map; - } - - 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) { - 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}`); - } - 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++; - } - } - return { source: text, ranges }; - } - - const newLineCharacter = "\n"; - function getRuleProvider(action?: (opts: FormatCodeSettings) => void) { - const options = { - indentSize: 4, - tabSize: 4, - newLineCharacter, - convertTabsToSpaces: true, - indentStyle: ts.IndentStyle.Smart, - insertSpaceAfterConstructor: false, - insertSpaceAfterCommaDelimiter: true, - insertSpaceAfterSemicolonInForStatements: true, - insertSpaceBeforeAndAfterBinaryOperators: true, - insertSpaceAfterKeywordsInControlFlowStatements: true, - insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, - insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, - insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false, - insertSpaceBeforeFunctionParenthesis: false, - placeOpenBraceOnNewLineForFunctions: false, - placeOpenBraceOnNewLineForControlBlocks: false, - }; - if (action) { - action(options); - } - const rulesProvider = new formatting.RulesProvider(); - rulesProvider.ensureUpToDate(options); - return rulesProvider; - } - - 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"); - if (!selectionRange) { - throw new Error(`Test ${s} does not specify selection range`); - } - const result = refactor.extractSymbol.getRangeToExtract(file, createTextSpanFromBounds(selectionRange.start, selectionRange.end)); - assert(result.targetRange === undefined, "failure expected"); - const sortedErrors = result.errors.map(e => e.messageText).sort(); - assert.deepEqual(sortedErrors, expectedErrors.sort(), "unexpected errors"); - }); - } - - 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"); - 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"); - if (expectedRange) { - let start: number, end: number; - if (ts.isArray(result.targetRange.range)) { - start = result.targetRange.range[0].getStart(f); - end = ts.lastOrUndefined(result.targetRange.range).getEnd(); - } - else { - start = result.targetRange.range.getStart(f); - end = result.targetRange.range.getEnd(); - } - assert.equal(start, expectedRange.start, "incorrect start of range"); - assert.equal(end, expectedRange.end, "incorrect end of range"); - } - else { - assert.isTrue(!result.targetRange, `expected range to extract to be undefined`); - } - } - describe("extractMethods", () => { - it("get extract range from selection", () => { - testExtractRange(` - [#| - [$|var x = 1; - var y = 2;|]|] - `); - testExtractRange(` - [#| - var x = 1; - var y = 2|]; - `); - testExtractRange(` - [#|var x = 1|]; - var y = 2; - `); - testExtractRange(` - if ([#|[#extracted|a && b && c && d|]|]) { - } - `); - testExtractRange(` - if [#|(a && b && c && d|]) { - } - `); - testExtractRange(` - if (a && b && c && d) { - [#| [$|var x = 1; - console.log(x);|] |] - } - `); - testExtractRange(` - [#| - if (a) { - return 100; - } |] - `); - testExtractRange(` - function foo() { - [#| [$|if (a) { - } - return 100|] |] - } - `); - testExtractRange(` - [#| - [$|l1: - if (x) { - break l1; - }|]|] - `); - testExtractRange(` - [#| - [$|l2: - { - if (x) { - } - break l2; - }|]|] - `); - testExtractRange(` - while (true) { - [#| if(x) { - } - break; |] - } - `); - testExtractRange(` - while (true) { - [#| if(x) { - } - continue; |] - } - `); - testExtractRange(` - l3: - { - [#| - if (x) { - } - break l3; |] - } - `); - testExtractRange(` - function f() { - while (true) { - [#| - if (x) { - return; - } |] - } - } - `); - testExtractRange(` - function f() { - while (true) { - [#| - [$|if (x) { - } - return;|] - |] - } - } - `); - testExtractRange(` - function f() { - return [#| [$|1 + 2|] |]+ 3; - } - } - `); - testExtractRange(` - function f() { - return [$|1 + [#|2 + 3|]|]; - } - } - `); - testExtractRange(` - function f() { - return [$|1 + 2 + [#|3 + 4|]|]; - } - } - `); - }); - - testExtractRangeFailed("extractRangeFailed1", - ` -namespace A { - function f() { - [#| - let x = 1 - if (x) { - return 10; - } - |] - } -} - `, - [ - "Cannot extract range containing conditional return statement." - ]); - - testExtractRangeFailed("extractRangeFailed2", - ` -namespace A { - function f() { - while (true) { - [#| - let x = 1 - if (x) { - break; - } - |] - } - } -} - `, - [ - "Cannot extract range containing conditional break or continue statements." - ]); - - testExtractRangeFailed("extractRangeFailed3", - ` -namespace A { - function f() { - while (true) { - [#| - let x = 1 - if (x) { - continue; - } - |] - } - } -} - `, - [ - "Cannot extract range containing conditional break or continue statements." - ]); - - testExtractRangeFailed("extractRangeFailed4", - ` -namespace A { - function f() { - l1: { - [#| - let x = 1 - if (x) { - break l1; - } - |] - } - } -} - `, - [ - "Cannot extract range containing labeled break or continue with target outside of the range." - ]); - - testExtractRangeFailed("extractRangeFailed5", - ` -namespace A { - function f() { - [#| - try { - f2() - return 10; - } - catch (e) { - } - |] - } - function f2() { - } -} - `, - [ - "Cannot extract range containing conditional return statement." - ]); - - testExtractRangeFailed("extractRangeFailed6", - ` -namespace A { - function f() { - [#| - try { - f2() - } - catch (e) { - return 10; - } - |] - } - function f2() { - } -} - `, - [ - "Cannot extract range containing conditional return statement." - ]); - - testExtractRangeFailed("extractRangeFailed7", - ` -function test(x: number) { - while (x) { - x--; - [#|break;|] - } -} - `, - [ - "Cannot extract range containing conditional break or continue statements." - ]); - - testExtractRangeFailed("extractRangeFailed8", - ` -function test(x: number) { - switch (x) { - case 1: - [#|break;|] - } -} - `, - [ - "Cannot extract range containing conditional break or continue statements." - ]); - - testExtractRangeFailed("extractRangeFailed9", - `var x = ([#||]1 + 2);`, - [ - "Cannot extract empty range." - ]); - - testExtractRangeFailed("extract-method-not-for-token-expression-statement", `[#|a|]`, ["Select more than a single identifier."]); - testExtractMethod("extractMethod1", `namespace A { let x = 1; @@ -776,50 +366,7 @@ function parsePrimaryExpression(): any { }`); }); - function testExtractMethod(caption: string, text: string) { - it(caption, () => { - Harness.Baseline.runBaseline(`extractMethod/${caption}.ts`, () => { - const t = extractTest(text); - const selectionRange = t.ranges.get("selection"); - if (!selectionRange) { - 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 projectService = projectSystem.createProjectService(host); - projectService.openClientFile(f.path); - const program = projectService.inferredProjects[0].getLanguageService().getProgram(); - const sourceFile = program.getSourceFile(f.path); - const context: RefactorContext = { - cancellationToken: { throwIfCancellationRequested() { }, isCancellationRequested() { return false; } }, - newLineCharacter, - program, - file: sourceFile, - startPosition: selectionRange.start, - endPosition: selectionRange.end, - rulesProvider: getRuleProvider() - }; - const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromBounds(selectionRange.start, selectionRange.end)); - assert.equal(rangeToExtract.errors, undefined, "expect no errors"); - const infos = refactor.extractSymbol.getAvailableActions(context); - const actions = find(infos, info => info.description === Diagnostics.Extract_function.message).actions; - const data: string[] = []; - data.push(`// ==ORIGINAL==`); - data.push(sourceFile.text); - for (const action of actions) { - const { renameLocation, edits } = refactor.extractSymbol.getEditsForAction(context, action.name); - assert.lengthOf(edits, 1); - data.push(`// ==SCOPE::${action.description}==`); - const newText = textChanges.applyChanges(sourceFile.text, edits[0].textChanges); - const newTextWithRename = newText.slice(0, renameLocation) + "/*RENAME*/" + newText.slice(renameLocation); - data.push(newTextWithRename); - } - return data.join(newLineCharacter); - }); - }); + testExtractSymbol(caption, text, "extractMethod", Diagnostics.Extract_function); } } diff --git a/src/harness/unittests/extractRanges.ts b/src/harness/unittests/extractRanges.ts new file mode 100644 index 00000000000..55535a6dc44 --- /dev/null +++ b/src/harness/unittests/extractRanges.ts @@ -0,0 +1,319 @@ +/// + +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"); + if (!selectionRange) { + throw new Error(`Test ${s} does not specify selection range`); + } + const result = refactor.extractSymbol.getRangeToExtract(file, createTextSpanFromBounds(selectionRange.start, selectionRange.end)); + assert(result.targetRange === undefined, "failure expected"); + const sortedErrors = result.errors.map(e => e.messageText).sort(); + assert.deepEqual(sortedErrors, expectedErrors.sort(), "unexpected errors"); + }); + } + + 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"); + 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"); + if (expectedRange) { + let start: number, end: number; + if (ts.isArray(result.targetRange.range)) { + start = result.targetRange.range[0].getStart(f); + end = ts.lastOrUndefined(result.targetRange.range).getEnd(); + } + else { + start = result.targetRange.range.getStart(f); + end = result.targetRange.range.getEnd(); + } + assert.equal(start, expectedRange.start, "incorrect start of range"); + assert.equal(end, expectedRange.end, "incorrect end of range"); + } + else { + assert.isTrue(!result.targetRange, `expected range to extract to be undefined`); + } + } + + describe("extractRanges", () => { + it("get extract range from selection", () => { + testExtractRange(` + [#| + [$|var x = 1; + var y = 2;|]|] + `); + testExtractRange(` + [#| + var x = 1; + var y = 2|]; + `); + testExtractRange(` + [#|var x = 1|]; + var y = 2; + `); + testExtractRange(` + if ([#|[#extracted|a && b && c && d|]|]) { + } + `); + testExtractRange(` + if [#|(a && b && c && d|]) { + } + `); + testExtractRange(` + if (a && b && c && d) { + [#| [$|var x = 1; + console.log(x);|] |] + } + `); + testExtractRange(` + [#| + if (a) { + return 100; + } |] + `); + testExtractRange(` + function foo() { + [#| [$|if (a) { + } + return 100|] |] + } + `); + testExtractRange(` + [#| + [$|l1: + if (x) { + break l1; + }|]|] + `); + testExtractRange(` + [#| + [$|l2: + { + if (x) { + } + break l2; + }|]|] + `); + testExtractRange(` + while (true) { + [#| if(x) { + } + break; |] + } + `); + testExtractRange(` + while (true) { + [#| if(x) { + } + continue; |] + } + `); + testExtractRange(` + l3: + { + [#| + if (x) { + } + break l3; |] + } + `); + testExtractRange(` + function f() { + while (true) { + [#| + if (x) { + return; + } |] + } + } + `); + testExtractRange(` + function f() { + while (true) { + [#| + [$|if (x) { + } + return;|] + |] + } + } + `); + testExtractRange(` + function f() { + return [#| [$|1 + 2|] |]+ 3; + } + } + `); + testExtractRange(` + function f() { + return [$|1 + [#|2 + 3|]|]; + } + } + `); + testExtractRange(` + function f() { + return [$|1 + 2 + [#|3 + 4|]|]; + } + } + `); + }); + + testExtractRangeFailed("extractRangeFailed1", + ` +namespace A { +function f() { + [#| + let x = 1 + if (x) { + return 10; + } + |] +} +} + `, + [ + "Cannot extract range containing conditional return statement." + ]); + + testExtractRangeFailed("extractRangeFailed2", + ` +namespace A { +function f() { + while (true) { + [#| + let x = 1 + if (x) { + break; + } + |] + } +} +} + `, + [ + "Cannot extract range containing conditional break or continue statements." + ]); + + testExtractRangeFailed("extractRangeFailed3", + ` +namespace A { +function f() { + while (true) { + [#| + let x = 1 + if (x) { + continue; + } + |] + } +} +} + `, + [ + "Cannot extract range containing conditional break or continue statements." + ]); + + testExtractRangeFailed("extractRangeFailed4", + ` +namespace A { +function f() { + l1: { + [#| + let x = 1 + if (x) { + break l1; + } + |] + } +} +} + `, + [ + "Cannot extract range containing labeled break or continue with target outside of the range." + ]); + + testExtractRangeFailed("extractRangeFailed5", + ` +namespace A { +function f() { + [#| + try { + f2() + return 10; + } + catch (e) { + } + |] +} +function f2() { +} +} + `, + [ + "Cannot extract range containing conditional return statement." + ]); + + testExtractRangeFailed("extractRangeFailed6", + ` +namespace A { +function f() { + [#| + try { + f2() + } + catch (e) { + return 10; + } + |] +} +function f2() { +} +} + `, + [ + "Cannot extract range containing conditional return statement." + ]); + + testExtractRangeFailed("extractRangeFailed7", + ` +function test(x: number) { +while (x) { + x--; + [#|break;|] +} +} + `, + [ + "Cannot extract range containing conditional break or continue statements." + ]); + + testExtractRangeFailed("extractRangeFailed8", + ` +function test(x: number) { +switch (x) { + case 1: + [#|break;|] +} +} + `, + [ + "Cannot extract range containing conditional break or continue statements." + ]); + + testExtractRangeFailed("extractRangeFailed9", + `var x = ([#||]1 + 2);`, + [ + "Cannot extract empty range." + ]); + + testExtractRangeFailed("extract-method-not-for-token-expression-statement", `[#|a|]`, ["Select more than a single identifier."]); + }); +} \ No newline at end of file diff --git a/src/harness/unittests/extractTestHelpers.ts b/src/harness/unittests/extractTestHelpers.ts new file mode 100644 index 00000000000..e619f9343d8 --- /dev/null +++ b/src/harness/unittests/extractTestHelpers.ts @@ -0,0 +1,177 @@ +/// +/// + +namespace ts { + export interface Range { + start: number; + end: number; + name: string; + } + + export interface Test { + 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) { + 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}`); + } + 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++; + } + } + return { source: text, ranges }; + } + + export const newLineCharacter = "\n"; + export function getRuleProvider(action?: (opts: FormatCodeSettings) => void) { + const options = { + indentSize: 4, + tabSize: 4, + newLineCharacter, + convertTabsToSpaces: true, + indentStyle: ts.IndentStyle.Smart, + insertSpaceAfterConstructor: false, + insertSpaceAfterCommaDelimiter: true, + insertSpaceAfterSemicolonInForStatements: true, + insertSpaceBeforeAndAfterBinaryOperators: true, + insertSpaceAfterKeywordsInControlFlowStatements: true, + insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, + insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, + insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false, + insertSpaceBeforeFunctionParenthesis: false, + placeOpenBraceOnNewLineForFunctions: false, + placeOpenBraceOnNewLineForControlBlocks: false, + }; + if (action) { + action(options); + } + const rulesProvider = new formatting.RulesProvider(); + rulesProvider.ensureUpToDate(options); + return rulesProvider; + } + + export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: DiagnosticMessage) { + it(caption, () => { + Harness.Baseline.runBaseline(`${baselineFolder}/${caption}.ts`, () => { + const t = extractTest(text); + const selectionRange = t.ranges.get("selection"); + if (!selectionRange) { + 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 projectService = projectSystem.createProjectService(host); + projectService.openClientFile(f.path); + const program = projectService.inferredProjects[0].getLanguageService().getProgram(); + const sourceFile = program.getSourceFile(f.path); + const context: RefactorContext = { + cancellationToken: { throwIfCancellationRequested() { }, isCancellationRequested() { return false; } }, + newLineCharacter, + program, + file: sourceFile, + startPosition: selectionRange.start, + endPosition: selectionRange.end, + rulesProvider: getRuleProvider() + }; + const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromBounds(selectionRange.start, selectionRange.end)); + assert.equal(rangeToExtract.errors, undefined, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); + const infos = refactor.extractSymbol.getAvailableActions(context); + const actions = find(infos, info => info.description === description.message).actions; + const data: string[] = []; + data.push(`// ==ORIGINAL==`); + data.push(sourceFile.text); + for (const action of actions) { + const { renameLocation, edits } = refactor.extractSymbol.getEditsForAction(context, action.name); + assert.lengthOf(edits, 1); + data.push(`// ==SCOPE::${action.description}==`); + const newText = textChanges.applyChanges(sourceFile.text, edits[0].textChanges); + const newTextWithRename = newText.slice(0, renameLocation) + "/*RENAME*/" + newText.slice(renameLocation); + data.push(newTextWithRename); + } + return data.join(newLineCharacter); + }); + }); + } + + export function testExtractSymbolFailed(caption: string, text: string, description: DiagnosticMessage) { + it(caption, () => { + const t = extractTest(text); + const selectionRange = t.ranges.get("selection"); + if (!selectionRange) { + 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 projectService = projectSystem.createProjectService(host); + projectService.openClientFile(f.path); + const program = projectService.inferredProjects[0].getLanguageService().getProgram(); + const sourceFile = program.getSourceFile(f.path); + const context: RefactorContext = { + cancellationToken: { throwIfCancellationRequested() { }, isCancellationRequested() { return false; } }, + newLineCharacter, + program, + file: sourceFile, + startPosition: selectionRange.start, + endPosition: selectionRange.end, + rulesProvider: getRuleProvider() + }; + const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromBounds(selectionRange.start, selectionRange.end)); + assert.isUndefined(rangeToExtract.errors, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); + const infos = refactor.extractSymbol.getAvailableActions(context); + assert.isUndefined(find(infos, info => info.description === description.message)); + }); + } +} \ No newline at end of file