angular-cn/packages/language-service/test/test_utils.ts

382 lines
13 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {setup} from '@angular/compiler-cli/test/test_support';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {Diagnostic, DiagnosticMessageChain, Diagnostics, Span} from '../src/types';
export type MockData = string | MockDirectory;
export type MockDirectory = {
[name: string]: MockData | undefined;
};
const angularts = /@angular\/(\w|\/|-)+\.tsx?$/;
const rxjsts = /rxjs\/(\w|\/)+\.tsx?$/;
const rxjsmetadata = /rxjs\/(\w|\/)+\.metadata\.json?$/;
const tsxfile = /\.tsx$/;
/* The missing cache does two things. First it improves performance of the
tests as it reduces the number of OS calls made during testing. Also it
improves debugging experience as fewer exceptions are raised allow you
to use stopping on all exceptions. */
const missingCache = new Map<string, boolean>();
const cacheUsed = new Set<string>();
const reportedMissing = new Set<string>();
/**
* The cache is valid if all the returned entries are empty.
*/
export function validateCache(): {exists: string[], unused: string[], reported: string[]} {
const exists: string[] = [];
const unused: string[] = [];
for (const fileName of iterableToArray(missingCache.keys())) {
if (fs.existsSync(fileName)) {
exists.push(fileName);
}
if (!cacheUsed.has(fileName)) {
unused.push(fileName);
}
}
return {exists, unused, reported: iterableToArray(reportedMissing.keys())};
}
missingCache.set('/node_modules/@angular/core.d.ts', true);
missingCache.set('/node_modules/@angular/animations.d.ts', true);
missingCache.set('/node_modules/@angular/platform-browser/animations.d.ts', true);
missingCache.set('/node_modules/@angular/common.d.ts', true);
missingCache.set('/node_modules/@angular/forms.d.ts', true);
missingCache.set('/node_modules/@angular/core/src/di/provider.metadata.json', true);
missingCache.set(
'/node_modules/@angular/core/src/change_detection/pipe_transform.metadata.json', true);
missingCache.set('/node_modules/@angular/core/src/reflection/types.metadata.json', true);
missingCache.set(
'/node_modules/@angular/core/src/reflection/platform_reflection_capabilities.metadata.json',
true);
missingCache.set('/node_modules/@angular/forms/src/directives/form_interface.metadata.json', true);
export class MockTypescriptHost implements ts.LanguageServiceHost {
private angularPath: string|undefined;
// TODO(issue/24571): remove '!'.
private nodeModulesPath !: string;
private scriptVersion = new Map<string, number>();
private overrides = new Map<string, string>();
private projectVersion = 0;
private options: ts.CompilerOptions;
private overrideDirectory = new Set<string>();
private existsCache = new Map<string, boolean>();
private fileCache = new Map<string, string|undefined>();
constructor(
private scriptNames: string[], private data: MockData,
private node_modules: string = 'node_modules', private myPath: typeof path = path) {
const support = setup();
this.nodeModulesPath = path.posix.join(support.basePath, 'node_modules');
this.angularPath = path.posix.join(this.nodeModulesPath, '@angular');
this.options = {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
emitDecoratorMetadata: true,
experimentalDecorators: true,
removeComments: false,
noImplicitAny: false,
lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'],
};
}
override(fileName: string, content: string) {
this.scriptVersion.set(fileName, (this.scriptVersion.get(fileName) || 0) + 1);
if (fileName.endsWith('.ts')) {
this.projectVersion++;
}
if (content) {
this.overrides.set(fileName, content);
this.overrideDirectory.add(path.dirname(fileName));
} else {
this.overrides.delete(fileName);
}
}
addScript(fileName: string, content: string) {
this.projectVersion++;
this.overrides.set(fileName, content);
this.overrideDirectory.add(path.dirname(fileName));
this.scriptNames.push(fileName);
}
forgetAngular() { this.angularPath = undefined; }
overrideOptions(cb: (options: ts.CompilerOptions) => ts.CompilerOptions) {
this.options = cb((Object as any).assign({}, this.options));
this.projectVersion++;
}
getCompilationSettings(): ts.CompilerOptions { return this.options; }
getProjectVersion(): string { return this.projectVersion.toString(); }
getScriptFileNames(): string[] { return this.scriptNames; }
getScriptVersion(fileName: string): string {
return (this.scriptVersion.get(fileName) || 0).toString();
}
getScriptSnapshot(fileName: string): ts.IScriptSnapshot|undefined {
const content = this.getFileContent(fileName);
if (content) return ts.ScriptSnapshot.fromString(content);
return undefined;
}
getCurrentDirectory(): string { return '/'; }
getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; }
directoryExists(directoryName: string): boolean {
if (this.overrideDirectory.has(directoryName)) return true;
let effectiveName = this.getEffectiveName(directoryName);
if (effectiveName === directoryName) {
return directoryExists(directoryName, this.data);
} else if (effectiveName == '/' + this.node_modules) {
return true;
} else {
return this.pathExists(effectiveName);
}
}
fileExists(fileName: string): boolean { return this.getRawFileContent(fileName) != null; }
readFile(path: string): string|undefined { return this.getRawFileContent(path); }
getMarkerLocations(fileName: string): {[name: string]: number}|undefined {
let content = this.getRawFileContent(fileName);
if (content) {
return getLocationMarkers(content);
}
}
getReferenceMarkers(fileName: string): ReferenceResult|undefined {
let content = this.getRawFileContent(fileName);
if (content) {
return getReferenceMarkers(content);
}
}
getFileContent(fileName: string): string|undefined {
const content = this.getRawFileContent(fileName);
if (content) return removeReferenceMarkers(removeLocationMarkers(content));
}
private getRawFileContent(fileName: string): string|undefined {
if (this.overrides.has(fileName)) {
return this.overrides.get(fileName);
}
let basename = path.basename(fileName);
if (/^lib.*\.d\.ts$/.test(basename)) {
let libPath = ts.getDefaultLibFilePath(this.getCompilationSettings());
return fs.readFileSync(this.myPath.join(path.dirname(libPath), basename), 'utf8');
} else {
if (missingCache.has(fileName)) {
cacheUsed.add(fileName);
return undefined;
}
const effectiveName = this.getEffectiveName(fileName);
if (effectiveName === fileName) {
return open(fileName, this.data);
} else if (
!fileName.match(angularts) && !fileName.match(rxjsts) && !fileName.match(rxjsmetadata) &&
!fileName.match(tsxfile)) {
if (this.fileCache.has(effectiveName)) {
return this.fileCache.get(effectiveName);
} else if (this.pathExists(effectiveName)) {
const content = fs.readFileSync(effectiveName, 'utf8');
this.fileCache.set(effectiveName, content);
return content;
} else {
missingCache.set(fileName, true);
reportedMissing.add(fileName);
cacheUsed.add(fileName);
}
}
}
}
private pathExists(path: string): boolean {
if (this.existsCache.has(path)) {
return this.existsCache.get(path) !;
}
const exists = fs.existsSync(path);
this.existsCache.set(path, exists);
return exists;
}
private getEffectiveName(name: string): string {
const node_modules = this.node_modules;
const at_angular = '/@angular';
if (name.startsWith('/' + node_modules)) {
if (this.nodeModulesPath && !name.startsWith('/' + node_modules + at_angular)) {
const result =
this.myPath.posix.join(this.nodeModulesPath, name.substr(node_modules.length + 1));
if (!name.match(rxjsts) && this.pathExists(result)) {
return result;
}
}
if (this.angularPath && name.startsWith('/' + node_modules + at_angular)) {
return this.myPath.posix.join(
this.angularPath, name.substr(node_modules.length + at_angular.length + 1));
}
}
return name;
}
}
function iterableToArray<T>(iterator: IterableIterator<T>) {
const result: T[] = [];
while (true) {
const next = iterator.next();
if (next.done) break;
result.push(next.value);
}
return result;
}
function find(fileName: string, data: MockData): MockData|undefined {
let names = fileName.split('/');
if (names.length && !names[0].length) names.shift();
let current = data;
for (let name of names) {
if (typeof current === 'string')
return undefined;
else
current = (<MockDirectory>current)[name] !;
if (!current) return undefined;
}
return current;
}
function open(fileName: string, data: MockData): string|undefined {
let result = find(fileName, data);
if (typeof result === 'string') {
return result;
}
return undefined;
}
function directoryExists(dirname: string, data: MockData): boolean {
let result = find(dirname, data);
return !!result && typeof result !== 'string';
}
const locationMarker = /\~\{(\w+(-\w+)*)\}/g;
function removeLocationMarkers(value: string): string {
return value.replace(locationMarker, '');
}
function getLocationMarkers(value: string): {[name: string]: number} {
value = removeReferenceMarkers(value);
let result: {[name: string]: number} = {};
let adjustment = 0;
value.replace(locationMarker, (match: string, name: string, _: any, index: number): string => {
result[name] = index - adjustment;
adjustment += match.length;
return '';
});
return result;
}
const referenceMarker = /«(((\w|\-)+)|([^ᐱ]*ᐱ(\w+)ᐱ.[^»]*))»/g;
export type ReferenceMarkers = {
[name: string]: Span[]
};
export interface ReferenceResult {
text: string;
definitions: ReferenceMarkers;
references: ReferenceMarkers;
}
function getReferenceMarkers(value: string): ReferenceResult {
const references: ReferenceMarkers = {};
const definitions: ReferenceMarkers = {};
value = removeLocationMarkers(value);
let adjustment = 0;
const text = value.replace(
referenceMarker, (match: string, text: string, reference: string, _: string,
definition: string, definitionName: string, index: number): string => {
const result = reference ? text : text.replace(/ᐱ/g, '');
const span: Span = {start: index - adjustment, end: index - adjustment + result.length};
const markers = reference ? references : definitions;
const name = reference || definitionName;
(markers[name] = (markers[name] || [])).push(span);
adjustment += match.length - result.length;
return result;
});
return {text, definitions, references};
}
function removeReferenceMarkers(value: string): string {
return value.replace(referenceMarker, (match, text) => text.replace(/ᐱ/g, ''));
}
export function noDiagnostics(diagnostics: Diagnostics) {
if (diagnostics && diagnostics.length) {
throw new Error(`Unexpected diagnostics: \n ${diagnostics.map(d => d.message).join('\n ')}`);
}
}
export function diagnosticMessageContains(
message: string | DiagnosticMessageChain, messageFragment: string): boolean {
if (typeof message == 'string') {
return message.indexOf(messageFragment) >= 0;
}
if (message.message.indexOf(messageFragment) >= 0) {
return true;
}
if (message.next) {
return diagnosticMessageContains(message.next, messageFragment);
}
return false;
}
export function findDiagnostic(diagnostics: Diagnostic[], messageFragment: string): Diagnostic|
undefined {
return diagnostics.find(d => diagnosticMessageContains(d.message, messageFragment));
}
export function includeDiagnostic(
diagnostics: Diagnostics, message: string, text?: string, len?: string): void;
export function includeDiagnostic(
diagnostics: Diagnostics, message: string, at?: number, len?: number): void;
export function includeDiagnostic(diagnostics: Diagnostics, message: string, p1?: any, p2?: any) {
expect(diagnostics).toBeDefined();
if (diagnostics) {
const diagnostic = findDiagnostic(diagnostics, message);
expect(diagnostic).toBeDefined(`no diagnostic contains '${message}`);
if (diagnostic && p1 != null) {
const at = typeof p1 === 'number' ? p1 : p2.indexOf(p1);
const len = typeof p2 === 'number' ? p2 : p1.length;
expect(diagnostic.span.start)
.toEqual(
at,
`expected message '${message}' was reported at ${diagnostic.span.start} but should be ${at}`);
if (len != null) {
expect(diagnostic.span.end - diagnostic.span.start)
.toEqual(len, `expected '${message}'s span length to be ${len}`);
}
}
}
}