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