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