• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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