• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import * as ts from "../_namespaces/ts";
2
3const enum ChangedPart {
4    none = 0,
5    references = 1 << 0,
6    importsAndExports = 1 << 1,
7    program = 1 << 2
8}
9
10export const newLine = "\r\n";
11
12export interface SourceFileWithText extends ts.SourceFile {
13    sourceText?: SourceText;
14}
15
16export interface NamedSourceText {
17    name: string;
18    text: SourceText;
19}
20
21export interface ProgramWithSourceTexts extends ts.Program {
22    sourceTexts?: readonly NamedSourceText[];
23    host: TestCompilerHost;
24}
25
26export interface TestCompilerHost extends ts.CompilerHost {
27    getTrace(): string[];
28}
29
30export class SourceText implements ts.IScriptSnapshot {
31    private fullText: string | undefined;
32
33    constructor(private references: string,
34        private importsAndExports: string,
35        private program: string,
36        private changedPart: ChangedPart = 0,
37        private version = 0) {
38    }
39
40    static New(references: string, importsAndExports: string, program: string): SourceText {
41        ts.Debug.assert(references !== undefined);
42        ts.Debug.assert(importsAndExports !== undefined);
43        ts.Debug.assert(program !== undefined);
44        return new SourceText(references + newLine, importsAndExports + newLine, program || "");
45    }
46
47    public getVersion(): number {
48        return this.version;
49    }
50
51    public updateReferences(newReferences: string): SourceText {
52        ts.Debug.assert(newReferences !== undefined);
53        return new SourceText(newReferences + newLine, this.importsAndExports, this.program, this.changedPart | ChangedPart.references, this.version + 1);
54    }
55    public updateImportsAndExports(newImportsAndExports: string): SourceText {
56        ts.Debug.assert(newImportsAndExports !== undefined);
57        return new SourceText(this.references, newImportsAndExports + newLine, this.program, this.changedPart | ChangedPart.importsAndExports, this.version + 1);
58    }
59    public updateProgram(newProgram: string): SourceText {
60        ts.Debug.assert(newProgram !== undefined);
61        return new SourceText(this.references, this.importsAndExports, newProgram, this.changedPart | ChangedPart.program, this.version + 1);
62    }
63
64    public getFullText() {
65        return this.fullText || (this.fullText = this.references + this.importsAndExports + this.program);
66    }
67
68    public getText(start: number, end: number): string {
69        return this.getFullText().substring(start, end);
70    }
71
72    getLength(): number {
73        return this.getFullText().length;
74    }
75
76    getChangeRange(oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange {
77        const oldText = oldSnapshot as SourceText;
78        let oldSpan: ts.TextSpan;
79        let newLength: number;
80        switch (oldText.changedPart ^ this.changedPart) {
81            case ChangedPart.references:
82                oldSpan = ts.createTextSpan(0, oldText.references.length);
83                newLength = this.references.length;
84                break;
85            case ChangedPart.importsAndExports:
86                oldSpan = ts.createTextSpan(oldText.references.length, oldText.importsAndExports.length);
87                newLength = this.importsAndExports.length;
88                break;
89            case ChangedPart.program:
90                oldSpan = ts.createTextSpan(oldText.references.length + oldText.importsAndExports.length, oldText.program.length);
91                newLength = this.program.length;
92                break;
93            default:
94                return ts.Debug.fail("Unexpected change");
95        }
96
97        return ts.createTextChangeRange(oldSpan, newLength);
98    }
99}
100
101function createSourceFileWithText(fileName: string, sourceText: SourceText, target: ts.ScriptTarget) {
102    const file = ts.createSourceFile(fileName, sourceText.getFullText(), target) as SourceFileWithText;
103    file.sourceText = sourceText;
104    file.version = "" + sourceText.getVersion();
105    return file;
106}
107
108export function createTestCompilerHost(texts: readonly NamedSourceText[], target: ts.ScriptTarget, oldProgram?: ProgramWithSourceTexts, useGetSourceFileByPath?: boolean) {
109    const files = ts.arrayToMap(texts, t => t.name, t => {
110        if (oldProgram) {
111            let oldFile = oldProgram.getSourceFile(t.name) as SourceFileWithText;
112            if (oldFile && oldFile.redirectInfo) {
113                oldFile = oldFile.redirectInfo.unredirected;
114            }
115            if (oldFile && oldFile.sourceText!.getVersion() === t.text.getVersion()) {
116                return oldFile;
117            }
118        }
119        return createSourceFileWithText(t.name, t.text, target);
120    });
121    const useCaseSensitiveFileNames = ts.sys && ts.sys.useCaseSensitiveFileNames;
122    const getCanonicalFileName = ts.createGetCanonicalFileName(useCaseSensitiveFileNames);
123    const trace: string[] = [];
124    const result: TestCompilerHost = {
125        trace: s => trace.push(s),
126        getTrace: () => trace,
127        getSourceFile: fileName => files.get(fileName),
128        getDefaultLibFileName: () => "lib.d.ts",
129        writeFile: ts.notImplemented,
130        getCurrentDirectory: () => "",
131        getDirectories: () => [],
132        getCanonicalFileName,
133        useCaseSensitiveFileNames: () => useCaseSensitiveFileNames,
134        getNewLine: () => ts.sys ? ts.sys.newLine : newLine,
135        fileExists: fileName => files.has(fileName),
136        readFile: fileName => {
137            const file = files.get(fileName);
138            return file && file.text;
139        },
140    };
141    if (useGetSourceFileByPath) {
142        const filesByPath = ts.mapEntries(files, (fileName, file) => [ts.toPath(fileName, "", getCanonicalFileName), file]);
143        result.getSourceFileByPath = (_fileName, path) => filesByPath.get(path);
144    }
145    return result;
146}
147
148export function newProgram(texts: NamedSourceText[], rootNames: string[], options: ts.CompilerOptions, useGetSourceFileByPath?: boolean): ProgramWithSourceTexts {
149    const host = createTestCompilerHost(texts, options.target!, /*oldProgram*/ undefined, useGetSourceFileByPath);
150    const program = ts.createProgram(rootNames, options, host) as ProgramWithSourceTexts;
151    program.sourceTexts = texts;
152    program.host = host;
153    return program;
154}
155
156export function updateProgram(oldProgram: ProgramWithSourceTexts, rootNames: readonly string[], options: ts.CompilerOptions, updater: (files: NamedSourceText[]) => void, newTexts?: NamedSourceText[], useGetSourceFileByPath?: boolean) {
157    if (!newTexts) {
158        newTexts = oldProgram.sourceTexts!.slice(0);
159    }
160    updater(newTexts);
161    const host = createTestCompilerHost(newTexts, options.target!, oldProgram, useGetSourceFileByPath);
162    const program = ts.createProgram(rootNames, options, host, oldProgram) as ProgramWithSourceTexts;
163    program.sourceTexts = newTexts;
164    program.host = host;
165    return program;
166}
167
168export function updateProgramText(files: readonly NamedSourceText[], fileName: string, newProgramText: string) {
169    const file = ts.find(files, f => f.name === fileName)!;
170    file.text = file.text.updateProgram(newProgramText);
171}
172
173export function checkResolvedTypeDirective(actual: ts.ResolvedTypeReferenceDirective, expected: ts.ResolvedTypeReferenceDirective) {
174    assert.equal(actual.resolvedFileName, expected.resolvedFileName, `'resolvedFileName': expected '${actual.resolvedFileName}' to be equal to '${expected.resolvedFileName}'`);
175    assert.equal(actual.primary, expected.primary, `'primary': expected '${actual.primary}' to be equal to '${expected.primary}'`);
176    return true;
177}
178
179function checkCache<T>(caption: string, program: ts.Program, fileName: string, expectedContent: ts.ESMap<string, T> | undefined, getCache: (f: ts.SourceFile) => ts.ModeAwareCache<T> | undefined, entryChecker: (expected: T, original: T) => boolean): void {
180    const file = program.getSourceFile(fileName);
181    assert.isTrue(file !== undefined, `cannot find file ${fileName}`);
182    const cache = getCache(file!);
183    if (expectedContent === undefined) {
184        assert.isTrue(cache === undefined, `expected ${caption} to be undefined`);
185    }
186    else {
187        assert.isTrue(cache !== undefined, `expected ${caption} to be set`);
188        assert.isTrue(mapEqualToCache(expectedContent, cache!, entryChecker), `contents of ${caption} did not match the expected contents.`);
189    }
190}
191
192/** True if the maps have the same keys and values. */
193function mapEqualToCache<T>(left: ts.ESMap<string, T>, right: ts.ModeAwareCache<T>, valuesAreEqual?: (left: T, right: T) => boolean): boolean {
194    // given the type mismatch (the tests never pass a cache), this'll never be true
195    if (left as any === right) {
196        return true;
197    }
198    if (!left || !right) {
199        return false;
200    }
201    const someInLeftHasNoMatch = ts.forEachEntry(left, (leftValue, leftKey) => {
202        if (!right.has(leftKey, /*mode*/ undefined)) {
203            return true;
204        }
205        const rightValue = right.get(leftKey, /*mode*/ undefined)!;
206        return !(valuesAreEqual ? valuesAreEqual(leftValue, rightValue) : leftValue === rightValue);
207    });
208    if (someInLeftHasNoMatch) {
209        return false;
210    }
211    let someInRightHasNoMatch = false;
212    right.forEach((_, rightKey) => someInRightHasNoMatch = someInRightHasNoMatch || !left.has(rightKey));
213    return !someInRightHasNoMatch;
214}
215
216export function checkResolvedModulesCache(program: ts.Program, fileName: string, expectedContent: ts.ESMap<string, ts.ResolvedModule | undefined> | undefined): void {
217    checkCache("resolved modules", program, fileName, expectedContent, f => f.resolvedModules, ts.checkResolvedModule);
218}
219
220export function checkResolvedTypeDirectivesCache(program: ts.Program, fileName: string, expectedContent: ts.ESMap<string, ts.ResolvedTypeReferenceDirective> | undefined): void {
221    checkCache("resolved type directives", program, fileName, expectedContent, f => f.resolvedTypeReferenceDirectiveNames, checkResolvedTypeDirective);
222}
223
224export function createResolvedModule(resolvedFileName: string, isExternalLibraryImport = false): ts.ResolvedModuleFull {
225    return { resolvedFileName, extension: ts.extensionFromPath(resolvedFileName), isExternalLibraryImport };
226}
227
228export function checkResolvedModule(actual: ts.ResolvedModuleFull | undefined, expected: ts.ResolvedModuleFull | undefined): boolean {
229    if (!expected) {
230        if (actual) {
231            assert.fail(actual, expected, "expected resolved module to be undefined");
232            return false;
233        }
234        return true;
235    }
236    else if (!actual) {
237        assert.fail(actual, expected, "expected resolved module to be defined");
238        return false;
239    }
240
241    assert.isTrue(actual.resolvedFileName === expected.resolvedFileName, `'resolvedFileName': expected '${actual.resolvedFileName}' to be equal to '${expected.resolvedFileName}'`);
242    assert.isTrue(actual.extension === expected.extension, `'ext': expected '${actual.extension}' to be equal to '${expected.extension}'`);
243    assert.isTrue(actual.isExternalLibraryImport === expected.isExternalLibraryImport, `'isExternalLibraryImport': expected '${actual.isExternalLibraryImport}' to be equal to '${expected.isExternalLibraryImport}'`);
244    return true;
245}
246
247export function checkResolvedModuleWithFailedLookupLocations(actual: ts.ResolvedModuleWithFailedLookupLocations, expectedResolvedModule: ts.ResolvedModuleFull, expectedFailedLookupLocations: string[]): void {
248    assert.isTrue(actual.resolvedModule !== undefined, "module should be resolved");
249    checkResolvedModule(actual.resolvedModule, expectedResolvedModule);
250    assert.deepEqual(actual.failedLookupLocations, expectedFailedLookupLocations, `Failed lookup locations should match - expected has ${expectedFailedLookupLocations.length}, actual has ${actual.failedLookupLocations.length}`);
251}