feat(language-service): add services to support editors (#12987)

This commit is contained in:
Chuck Jazdzewski
2016-11-22 09:10:23 -08:00
committed by GitHub
parent ef96763fa4
commit 519a324454
37 changed files with 6688 additions and 9 deletions

View File

@ -0,0 +1,236 @@
/**
* @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 'reflect-metadata';
import * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {Completions, Diagnostic, Diagnostics} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost, includeDiagnostic, noDiagnostics} from './test_utils';
describe('completions', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be able to get entity completions',
() => { contains('/app/test.ng', 'entity-amp', '&', '>', '<', 'ι'); });
it('should be able to return html elements', () => {
let htmlTags = ['h1', 'h2', 'div', 'span'];
let locations = ['empty', 'start-tag-h1', 'h1-content', 'start-tag', 'start-tag-after-h'];
for (let location of locations) {
contains('/app/test.ng', location, ...htmlTags);
}
});
it('should be able to return element diretives',
() => { contains('/app/test.ng', 'empty', 'my-app'); });
it('should be able to return h1 attributes',
() => { contains('/app/test.ng', 'h1-after-space', 'id', 'dir', 'lang', 'onclick'); });
it('should be able to find common angular attributes',
() => { contains('/app/test.ng', 'div-attributes', '(click)', '[ngClass]'); });
it('should be able to get completions in some random garbage', () => {
const fileName = '/app/test.ng';
mockHost.override(fileName, ' > {{tle<\n {{retl ><bel/beled}}di>\n la</b </d &a ');
expect(() => ngService.getCompletionsAt(fileName, 31)).not.toThrow();
mockHost.override(fileName, undefined);
});
it('should be able to infer the type of a ngForOf', () => {
addCode(
`
interface Person {
name: string,
street: string
}
@Component({template: '<div *ngFor="let person of people">{{person.~{name}name}}</div'})
export class MyComponent {
people: Person[]
}`,
() => { contains('/app/app.component.ts', 'name', 'name', 'street'); });
});
it('should be able to infer the type of a ngForOf with an async pipe', () => {
addCode(
`
interface Person {
name: string,
street: string
}
@Component({template: '<div *ngFor="let person of people | async">{{person.~{name}name}}</div'})
export class MyComponent {
people: Promise<Person[]>;
}`,
() => { contains('/app/app.component.ts', 'name', 'name', 'street'); });
});
it('should be able to complete every character in the file', () => {
const fileName = '/app/test.ng';
expect(() => {
let chance = 0.05;
let requests = 0;
function tryCompletionsAt(position: number) {
try {
if (Math.random() < chance) {
ngService.getCompletionsAt(fileName, position);
requests++;
}
} catch (e) {
// Emit enough diagnostic information to reproduce the error.
console.log(
`Position: ${position}\nContent: "${mockHost.getFileContent(fileName)}"\nStack:\n${e.stack}`);
throw e;
}
}
try {
const originalContent = mockHost.getFileContent(fileName);
// For each character in the file, add it to the file and request a completion after it.
for (let index = 0, len = originalContent.length; index < len; index++) {
const content = originalContent.substr(0, index);
mockHost.override(fileName, content);
tryCompletionsAt(index);
}
// For the complete file, try to get a completion at every character.
mockHost.override(fileName, originalContent);
for (let index = 0, len = originalContent.length; index < len; index++) {
tryCompletionsAt(index);
}
// Delete random characters in the file until we get an empty file.
let content = originalContent;
while (content.length > 0) {
const deleteIndex = Math.floor(Math.random() * content.length);
content = content.slice(0, deleteIndex - 1) + content.slice(deleteIndex + 1);
mockHost.override(fileName, content);
const requestIndex = Math.floor(Math.random() * content.length);
tryCompletionsAt(requestIndex);
}
// Build up the string from zero asking for a completion after every char
buildUp(originalContent, (text, position) => {
mockHost.override(fileName, text);
tryCompletionsAt(position);
});
} finally {
mockHost.override(fileName, undefined);
}
}).not.toThrow();
});
describe('with regression tests', () => {
it('should not crash with an incomplete component', () => {
expect(() => {
const code = `
@Component({
template: '~{inside-template}'
})
export class MyComponent {
}`;
addCode(code, fileName => { contains(fileName, 'inside-template', 'h1'); });
}).not.toThrow();
});
it('should hot crash with an incomplete class', () => {
expect(() => {
addCode('\nexport class', fileName => { ngHost.updateAnalyzedModules(); });
}).not.toThrow();
});
});
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
const fileName = '/app/app.component.ts';
const originalContent = mockHost.getFileContent(fileName);
const newContent = originalContent + code;
mockHost.override(fileName, originalContent + code);
try {
cb(fileName, newContent);
} finally {
mockHost.override(fileName, undefined);
}
}
function contains(fileName: string, locationMarker: string, ...names: string[]) {
let location = mockHost.getMarkerLocations(fileName)[locationMarker];
if (location == null) {
throw new Error(`No marker ${locationMarker} found.`);
}
expectEntries(locationMarker, ngService.getCompletionsAt(fileName, location), ...names);
}
});
function expectEntries(locationMarker: string, completions: Completions, ...names: string[]) {
let entries: {[name: string]: boolean} = {};
if (!completions) {
throw new Error(
`Expected result from ${locationMarker} to include ${names.join(', ')} but no result provided`);
}
if (!completions.length) {
throw new Error(
`Expected result from ${locationMarker} to include ${names.join(', ')} an empty result provided`);
} else {
for (let entry of completions) {
entries[entry.name] = true;
}
let missing = names.filter(name => !entries[name]);
if (missing.length) {
throw new Error(
`Expected result from ${locationMarker} to include at least one of the following, ${missing.join(', ')}, in the list of entries ${completions.map(entry => entry.name).join(', ')}`);
}
}
}
function buildUp(originalText: string, cb: (text: string, position: number) => void) {
let count = originalText.length;
let inString: boolean[] = (new Array(count)).fill(false);
let unused: number[] = (new Array(count)).fill(1).map((v, i) => i);
function getText() {
return new Array(count)
.fill(1)
.map((v, i) => i)
.filter(i => inString[i])
.map(i => originalText[i])
.join('');
}
function randomUnusedIndex() { return Math.floor(Math.random() * unused.length); }
while (unused.length > 0) {
let unusedIndex = randomUnusedIndex();
let index = unused[unusedIndex];
if (index == null) throw new Error('Internal test buildup error');
if (inString[index]) throw new Error('Internal test buildup error');
inString[index] = true;
unused.splice(unusedIndex, 1);
let text = getText();
let position =
inString.filter((_, i) => i <= index).map(v => v ? 1 : 0).reduce((p, v) => p + v, 0);
cb(text, position);
}
}

View File

@ -0,0 +1,169 @@
/**
* @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 * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {Completions, Diagnostic, Diagnostics, Span} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost,} from './test_utils';
describe('definitions', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be able to find field in an interpolation', () => {
localReference(
` @Component({template: '{{«name»}}'}) export class MyComponent { «∆name∆: string;» }`);
});
it('should be able to find a field in a attribute reference', () => {
localReference(
` @Component({template: '<input [(ngModel)]="«name»">'}) export class MyComponent { «∆name∆: string;» }`);
});
it('should be able to find a method from a call', () => {
localReference(
` @Component({template: '<div (click)="«myClick»();"></div>'}) export class MyComponent { «∆myClick∆() { }»}`);
});
it('should be able to find a field reference in an *ngIf', () => {
localReference(
` @Component({template: '<div *ngIf="«include»"></div>'}) export class MyComponent { «∆include∆ = true;»}`);
});
it('should be able to find a reference to a component', () => {
reference(
'parsing-cases.ts',
` @Component({template: '<«test-comp»></test-comp>'}) export class MyComponent { }`);
});
it('should be able to find an event provider', () => {
reference(
'/app/parsing-cases.ts', 'test',
` @Component({template: '<test-comp («test»)="myHandler()"></div>'}) export class MyComponent { myHandler() {} }`);
});
it('should be able to find an input provider', () => {
reference(
'/app/parsing-cases.ts', 'tcName',
` @Component({template: '<test-comp [«tcName»]="name"></div>'}) export class MyComponent { name = 'my name'; }`);
});
it('should be able to find a pipe', () => {
reference(
'async_pipe.d.ts',
` @Component({template: '<div *ngIf="input | «async»"></div>'}) export class MyComponent { input: EventEmitter; }`);
});
function localReference(code: string) {
addCode(code, fileName => {
const refResult = mockHost.getReferenceMarkers(fileName);
for (const name in refResult.references) {
const references = refResult.references[name];
const definitions = refResult.definitions[name];
expect(definitions).toBeDefined(); // If this fails the test data is wrong.
for (const reference of references) {
const definition = ngService.getDefinitionAt(fileName, reference.start);
if (definition) {
definition.forEach(d => expect(d.fileName).toEqual(fileName));
const match = matchingSpan(definition.map(d => d.span), definitions);
if (!match) {
throw new Error(
`Expected one of ${stringifySpans(definition.map(d => d.span))} to match one of ${stringifySpans(definitions)}`);
}
} else {
throw new Error('Expected a definition');
}
}
}
});
}
function reference(referencedFile: string, code: string): void;
function reference(referencedFile: string, span: Span, code: string): void;
function reference(referencedFile: string, definition: string, code: string): void;
function reference(referencedFile: string, p1?: any, p2?: any): void {
const code: string = p2 ? p2 : p1;
const definition: string = p2 ? p1 : undefined;
let span: Span = p2 && p1.start != null ? p1 : undefined;
if (definition && !span) {
const referencedFileMarkers = mockHost.getReferenceMarkers(referencedFile);
expect(referencedFileMarkers).toBeDefined(); // If this fails the test data is wrong.
const spans = referencedFileMarkers.definitions[definition];
expect(spans).toBeDefined(); // If this fails the test data is wrong.
span = spans[0];
}
addCode(code, fileName => {
const refResult = mockHost.getReferenceMarkers(fileName);
let tests = 0;
for (const name in refResult.references) {
const references = refResult.references[name];
expect(reference).toBeDefined(); // If this fails the test data is wrong.
for (const reference of references) {
tests++;
const definition = ngService.getDefinitionAt(fileName, reference.start);
if (definition) {
definition.forEach(d => {
if (d.fileName.indexOf(referencedFile) < 0) {
throw new Error(
`Expected reference to file ${referencedFile}, received ${d.fileName}`);
}
if (span) {
expect(d.span).toEqual(span);
}
});
} else {
throw new Error('Expected a definition');
}
}
}
if (!tests) {
throw new Error('Expected at least one reference (test data error)');
}
});
}
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
const fileName = '/app/app.component.ts';
const originalContent = mockHost.getFileContent(fileName);
const newContent = originalContent + code;
mockHost.override(fileName, originalContent + code);
try {
cb(fileName, newContent);
} finally {
mockHost.override(fileName, undefined);
}
}
});
function matchingSpan(aSpans: Span[], bSpans: Span[]): Span {
for (const a of aSpans) {
for (const b of bSpans) {
if (a.start == b.start && a.end == b.end) {
return a;
}
}
}
}
function stringifySpan(span: Span) {
return span ? `(${span.start}-${span.end})` : '<undefined>';
}
function stringifySpans(spans: Span[]) {
return spans ? `[${spans.map(stringifySpan).join(', ')}]` : '<empty>';
}

View File

@ -0,0 +1,136 @@
/**
* @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 * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {Completions, Diagnostic, Diagnostics} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost, includeDiagnostic, noDiagnostics} from './test_utils';
describe('diagnostics', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be no diagnostics for test.ng',
() => { expect(ngService.getDiagnostics('/app/test.ng')).toEqual([]); });
describe('for semantic errors', () => {
const fileName = '/app/test.ng';
function diagnostics(template: string): Diagnostics {
try {
mockHost.override(fileName, template);
return ngService.getDiagnostics(fileName);
} finally {
mockHost.override(fileName, undefined);
}
}
function accept(template: string) { noDiagnostics(diagnostics(template)); }
function reject(template: string, message: string): void;
function reject(template: string, message: string, at: string): void;
function reject(template: string, message: string, location: string): void;
function reject(template: string, message: string, location: string, len: number): void;
function reject(template: string, message: string, at?: number | string, len?: number): void {
if (typeof at == 'string') {
len = at.length;
at = template.indexOf(at);
}
includeDiagnostic(diagnostics(template), message, at, len);
}
describe('with $event', () => {
it('should accept an event',
() => { accept('<div (click)="myClick($event)">Click me!</div>'); });
it('should reject it when not in an event binding', () => {
reject('<div [tabIndex]="$event"></div>', '\'$event\' is not defined', '$event');
});
});
});
describe('with regression tests', () => {
it('should not crash with a incomplete *ngFor', () => {
expect(() => {
const code =
'\n@Component({template: \'<div *ngFor></div> ~{after-div}\'}) export class MyComponent {}';
addCode(code, fileName => { ngService.getDiagnostics(fileName); });
}).not.toThrow();
});
it('should report a component not in a module', () => {
const code = '\n@Component({template: \'<div></div>\'}) export class MyComponent {}';
addCode(code, (fileName, content) => {
const diagnostics = ngService.getDiagnostics(fileName);
const offset = content.lastIndexOf('@Component') + 1;
const len = 'Component'.length;
includeDiagnostic(
diagnostics, 'Component \'MyComponent\' is not included in a module', offset, len);
});
});
it('should not report an error for a form\'s host directives', () => {
const code = '\n@Component({template: \'<form></form>\'}) export class MyComponent {}';
addCode(code, (fileName, content) => {
const diagnostics = ngService.getDiagnostics(fileName);
onlyModuleDiagnostics(diagnostics);
});
});
it('should not throw getting diagnostics for an index expression', () => {
const code =
` @Component({template: '<a *ngIf="(auth.isAdmin | async) || (event.leads && event.leads[(auth.uid | async)])"></a>'}) export class MyComponent {}`;
addCode(
code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); });
});
it('should not throw using a directive with no value', () => {
const code =
` @Component({template: '<form><input [(ngModel)]="name" required /></form>'}) export class MyComponent { name = 'some name'; }`;
addCode(
code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); });
});
it('should report an error for invalid metadata', () => {
const code =
` @Component({template: '', provider: [{provide: 'foo', useFactor: () => 'foo' }]}) export class MyComponent { name = 'some name'; }`;
addCode(code, (fileName, content) => {
const diagnostics = ngService.getDiagnostics(fileName);
includeDiagnostic(
diagnostics, 'Function calls are not supported.', '() => \'foo\'', content);
});
});
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
const fileName = '/app/app.component.ts';
const originalContent = mockHost.getFileContent(fileName);
const newContent = originalContent + code;
mockHost.override(fileName, originalContent + code);
try {
cb(fileName, newContent);
} finally {
mockHost.override(fileName, undefined);
}
}
function onlyModuleDiagnostics(diagnostics: Diagnostics) {
// Expect only the 'MyComponent' diagnostic
expect(diagnostics.length).toBe(1);
expect(diagnostics[0].message.indexOf('MyComponent') >= 0).toBeTruthy();
}
});
});

View File

@ -0,0 +1,105 @@
/**
* @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 'reflect-metadata';
import * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {Hover, HoverTextSection} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost, includeDiagnostic, noDiagnostics} from './test_utils';
describe('hover', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be able to find field in an interpolation', () => {
hover(
` @Component({template: '{{«name»}}'}) export class MyComponent { name: string; }`,
'property name of MyComponent');
});
it('should be able to find a field in a attribute reference', () => {
hover(
` @Component({template: '<input [(ngModel)]="«name»">'}) export class MyComponent { name: string; }`,
'property name of MyComponent');
});
it('should be able to find a method from a call', () => {
hover(
` @Component({template: '<div (click)="«∆myClick∆()»;"></div>'}) export class MyComponent { myClick() { }}`,
'method myClick of MyComponent');
});
it('should be able to find a field reference in an *ngIf', () => {
hover(
` @Component({template: '<div *ngIf="«include»"></div>'}) export class MyComponent { include = true;}`,
'property include of MyComponent');
});
it('should be able to find a reference to a component', () => {
hover(
` @Component({template: '«<∆test∆-comp></test-comp>»'}) export class MyComponent { }`,
'component TestComponent');
});
it('should be able to find an event provider', () => {
hover(
` @Component({template: '<test-comp «(∆test∆)="myHandler()"»></div>'}) export class MyComponent { myHandler() {} }`,
'event testEvent of TestComponent');
});
it('should be able to find an input provider', () => {
hover(
` @Component({template: '<test-comp «[∆tcName∆]="name"»></div>'}) export class MyComponent { name = 'my name'; }`,
'property name of TestComponent');
});
function hover(code: string, hoverText: string) {
addCode(code, fileName => {
let tests = 0;
const markers = mockHost.getReferenceMarkers(fileName);
const keys = Object.keys(markers.references).concat(Object.keys(markers.definitions));
for (const referenceName of keys) {
const references = (markers.references[referenceName] ||
[]).concat(markers.definitions[referenceName] || []);
for (const reference of references) {
tests++;
const hover = ngService.getHoverAt(fileName, reference.start);
if (!hover) throw new Error(`Expected a hover at location ${reference.start}`);
expect(hover.span).toEqual(reference);
expect(toText(hover)).toEqual(hoverText);
}
}
expect(tests).toBeGreaterThan(0); // If this fails the test is wrong.
});
}
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
const fileName = '/app/app.component.ts';
const originalContent = mockHost.getFileContent(fileName);
const newContent = originalContent + code;
mockHost.override(fileName, originalContent + code);
try {
cb(fileName, newContent);
} finally {
mockHost.override(fileName, undefined);
}
}
function toText(hover: Hover): string { return hover.text.map(h => h.text).join(''); }
});

View File

@ -0,0 +1,52 @@
/**
* @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 {DomElementSchemaRegistry} from '@angular/compiler';
import {SchemaInformation} from '../src/html_info';
describe('html_info', () => {
const domRegistry = new DomElementSchemaRegistry();
it('should have the same elements as the dom registry', () => {
// If this test fails, replace the SCHEMA constant in html_info with the one
// from dom_element_schema_registry and also verify the code to interpret
// the schema is the same.
const domElements = domRegistry.allKnownElementNames();
const infoElements = SchemaInformation.instance.allKnownElements();
const uniqueToDom = uniqueElements(infoElements, domElements);
const uniqueToInfo = uniqueElements(domElements, infoElements);
expect(uniqueToDom).toEqual([]);
expect(uniqueToInfo).toEqual([]);
});
it('should have at least a sub-set of properties', () => {
const elements = SchemaInformation.instance.allKnownElements();
for (const element of elements) {
for (const prop of SchemaInformation.instance.propertiesOf(element)) {
expect(domRegistry.hasProperty(element, prop, []));
}
}
});
});
function uniqueElements<T>(a: T[], b: T[]): T[] {
const s = new Set<T>();
for (const aItem of a) {
s.add(aItem);
}
const result: T[] = [];
const reported = new Set<T>();
for (const bItem of b) {
if (!s.has(bItem) && !reported.has(bItem)) {
reported.add(bItem);
result.push(bItem);
}
}
return result;
}

View File

@ -0,0 +1,32 @@
/**
* @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 * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {Completions, Diagnostic, Diagnostics} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost} from './test_utils';
describe('references', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be able to get template references',
() => { expect(() => ngService.getTemplateReferences()).not.toThrow(); });
it('should be able to determine that test.ng is a template reference',
() => { expect(ngService.getTemplateReferences()).toContain('/app/test.ng'); });
});

View File

@ -0,0 +1,231 @@
/**
* @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 {MockData} from './test_utils';
export const toh = {
app: {
'app.component.ts': `import { Component } from '@angular/core';
export class Hero {
id: number;
name: string;
}
@Component({
selector: 'my-app',
template: \`~{empty}
<~{start-tag}h~{start-tag-after-h}1~{start-tag-h1} ~{h1-after-space}>~{h1-content} {{~{sub-start}title~{sub-end}}}</h1>
~{after-h1}<h2>{{~{h2-hero}hero.~{h2-name}name}} details!</h2>
<div><label>id: </label>{{~{label-hero}hero.~{label-id}id}}</div>
<div ~{div-attributes}>
<label>name: </label>
</div>
&~{entity-amp}amp;
\`
})
export class AppComponent {
title = 'Tour of Heroes';
hero: Hero = {
id: 1,
name: 'Windstorm'
};
private internal: string;
}`,
'main.ts': `
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { CaseIncompleteOpen, CaseMissingClosing, CaseUnknown, Pipes, TemplateReference, NoValueAttribute,
AttributeBinding, StringModel,PropertyBinding, EventBinding, TwoWayBinding, EmptyInterpolation,
ForOfEmpty, ForLetIEqual, ForOfLetEmpty, ForUsingComponent, References, TestComponent} from './parsing-cases';
import { WrongFieldReference, WrongSubFieldReference, PrivateReference, ExpectNumericType, LowercasePipe } from './expression-cases';
import { UnknownPeople, UnknownEven, UnknownTrackBy } from './ng-for-cases';
import { ShowIf } from './ng-if-cases';
@NgModule({
imports: [CommonModule, FormsModule],
declarations: [AppComponent, CaseIncompleteOpen, CaseMissingClosing, CaseUnknown, Pipes, TemplateReference, NoValueAttribute,
AttributeBinding, StringModel, PropertyBinding, EventBinding, TwoWayBinding, EmptyInterpolation, ForOfEmpty, ForOfLetEmpty,
ForLetIEqual, ForUsingComponent, References, TestComponent, WrongFieldReference, WrongSubFieldReference, PrivateReference,
ExpectNumericType, UnknownPeople, UnknownEven, UnknownTrackBy, ShowIf, LowercasePipe]
})
export class AppModule {}
declare function bootstrap(v: any): void;
bootstrap(AppComponent);
`,
'parsing-cases.ts': `
import {Component, Directive, Input, Output, EventEmitter} from '@angular/core';
import {Hero} from './app.component';
@Component({template: '<h1>Some <~{incomplete-open-lt}a~{incomplete-open-a} ~{incomplete-open-attr} text</h1>'})
export class CaseIncompleteOpen {}
@Component({template: '<h1>Some <a> ~{missing-closing} text</h1>'})
export class CaseMissingClosing {}
@Component({template: '<h1>Some <unknown ~{unknown-element}> text</h1>'})
export class CaseUnknown {}
@Component({template: '<h1>{{data | ~{before-pipe}lowe~{in-pipe}rcase~{after-pipe} }}'})
export class Pipes {
data = 'Some string';
}
@Component({template: '<h1 h~{no-value-attribute}></h1>'})
export class NoValueAttribute {}
@Component({template: '<h1 model="~{attribute-binding-model}test"></h1>'})
export class AttributeBinding {
test: string;
}
@Component({template: '<h1 [model]="~{property-binding-model}test"></h1>'})
export class PropertyBinding {
test: string;
}
@Component({template: '<h1 (model)="~{event-binding-model}modelChanged()"></h1>'})
export class EventBinding {
test: string;
modelChanged() {}
}
@Component({template: '<h1 [(model)]="~{two-way-binding-model}test"></h1>'})
export class TwoWayBinding {
test: string;
}
@Directive({selector: '[string-model]'})
export class StringModel {
@Input() model: string;
@Output() modelChanged: EventEmitter<string>;
}
interface Person {
name: string;
age: number
}
@Component({template: '<div *ngFor="~{for-empty}"></div>'})
export class ForOfEmpty {}
@Component({template: '<div *ngFor="let ~{for-let-empty}"></div>'})
export class ForOfLetEmpty {}
@Component({template: '<div *ngFor="let i = ~{for-let-i-equal}"></div>'})
export class ForLetIEqual {}
@Component({template: '<div *ngFor="~{for-let}let ~{for-person}person ~{for-of}of ~{for-people}people"> <span>Name: {{~{for-interp-person}person.~{for-interp-name}name}}</span><span>Age: {{person.~{for-interp-age}age}}</span></div>'})
export class ForUsingComponent {
people: Person[];
}
@Component({template: '<div #div> <test-comp #test1> {{~{test-comp-content}}} {{test1.~{test-comp-after-test}name}} {{div.~{test-comp-after-div}.innerText}} </test-comp> </div> <test-comp #test2></test-comp>'})
export class References {}
@Component({selector: 'test-comp', template: '<div>Testing: {{name}}</div>'})
export class TestComponent {
«@Input('∆tcName∆') name = 'test';»
«@Output('∆test∆') testEvent = new EventEmitter();»
}
@Component({templateUrl: 'test.ng'})
export class TemplateReference {
title = 'Some title';
hero: Hero = {
id: 1,
name: 'Windstorm'
};
myClick(event: any) {
}
}
@Component({template: '{{~{empty-interpolation}}}'})
export class EmptyInterpolation {
title = 'Some title';
subTitle = 'Some sub title';
}
`,
'expression-cases.ts': `
import {Component} from '@angular/core';
export interface Person {
name: string;
age: number;
}
@Component({template: '{{~{foo}foo~{foo-end}}}'})
export class WrongFieldReference {
bar = 'bar';
}
@Component({template: '{{~{nam}person.nam~{nam-end}}}'})
export class WrongSubFieldReference {
person: Person = { name: 'Bob', age: 23 };
}
@Component({template: '{{~{myField}myField~{myField-end}}}'})
export class PrivateReference {
private myField = 'My Field';
}
@Component({template: '{{~{mod}"a" ~{mod-end}% 2}}'})
export class ExpectNumericType {}
@Component({template: '{{ (name | lowercase).~{string-pipe}substring }}'})
export class LowercasePipe {
name: string;
}
`,
'ng-for-cases.ts': `
import {Component} from '@angular/core';
export interface Person {
name: string;
age: number;
}
@Component({template: '<div *ngFor="let person of ~{people_1}people_1~{people_1-end}"> <span>{{person.name}}</span> </div>'})
export class UnknownPeople {}
@Component({template: '<div ~{even_1}*ngFor="let person of people; let e = even_1"~{even_1-end}><span>{{person.name}}</span> </div>'})
export class UnknownEven {
people: Person[];
}
@Component({template: '<div *ngFor="let person of people; trackBy ~{trackBy_1}trackBy_1~{trackBy_1-end}"><span>{{person.name}}</span> </div>'})
export class UnknownTrackBy {
people: Person[];
}
`,
'ng-if-cases.ts': `
import {Component} from '@angular/core';
@Component({template: '<div ~{implicit}*ngIf="show; let l"~{implicit-end}>Showing now!</div>'})
export class ShowIf {
show = false;
}
`,
'test.ng': `~{empty}
<~{start-tag}h~{start-tag-after-h}1~{start-tag-h1} ~{h1-after-space}>~{h1-content} {{~{sub-start}title~{sub-end}}}</h1>
~{after-h1}<h2>{{~{h2-hero}hero.~{h2-name}name}} details!</h2>
<div><label>id: </label>{{~{label-hero}hero.~{label-id}id}}</div>
<div ~{div-attributes}>
<label>name: </label>
</div>
&~{entity-amp}amp;
`
}
};

View File

@ -0,0 +1,320 @@
/**
* @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
*/
/// <reference path="../../../../node_modules/@types/node/index.d.ts" />
/// <reference path="../../../../node_modules/@types/jasmine/index.d.ts" />
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {Diagnostic, 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/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;
private nodeModulesPath: string;
private scriptVersion = new Map<string, number>();
private overrides = new Map<string, string>();
private projectVersion = 0;
constructor(private scriptNames: string[], private data: MockData) {
let angularIndex = module.filename.indexOf('@angular');
if (angularIndex >= 0)
this.angularPath =
module.filename.substr(0, angularIndex).replace('/all/', '/packages-dist/');
let distIndex = module.filename.indexOf('/dist/all');
if (distIndex >= 0)
this.nodeModulesPath = path.join(module.filename.substr(0, distIndex), 'node_modules');
}
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);
} else {
this.overrides.delete(fileName);
}
}
getCompilationSettings(): ts.CompilerOptions {
return {
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'],
};
}
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 {
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 {
let effectiveName = this.getEffectiveName(directoryName);
if (effectiveName === directoryName)
return directoryExists(directoryName, this.data);
else
return fs.existsSync(effectiveName);
}
getMarkerLocations(fileName: string): {[name: string]: number}|undefined {
let content = this.getRawFileContent(fileName);
if (content) {
return getLocationMarkers(content);
}
}
getReferenceMarkers(fileName: string): ReferenceResult {
let content = this.getRawFileContent(fileName);
if (content) {
return getReferenceMarkers(content);
}
}
getFileContent(fileName: string): string {
const content = this.getRawFileContent(fileName);
if (content) return removeReferenceMarkers(removeLocationMarkers(content));
}
private getRawFileContent(fileName: string): string {
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(path.join(path.dirname(libPath), basename), 'utf8');
} else {
if (missingCache.has(fileName)) {
cacheUsed.add(fileName);
return undefined;
}
let 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 (fs.existsSync(effectiveName)) {
return fs.readFileSync(effectiveName, 'utf8');
} else {
missingCache.set(fileName, true);
reportedMissing.add(fileName);
cacheUsed.add(fileName);
}
}
}
}
private getEffectiveName(name: string): string {
const node_modules = 'node_modules';
const at_angular = '/@angular';
if (name.startsWith('/' + node_modules)) {
if (this.nodeModulesPath && !name.startsWith('/' + node_modules + at_angular)) {
let result = path.join(this.nodeModulesPath, name.substr(node_modules.length + 1));
if (!name.match(rxjsts))
if (fs.existsSync(result)) {
return result;
}
}
if (this.angularPath && name.startsWith('/' + node_modules + at_angular)) {
return path.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;
const definitionMarkerGroup = 1;
const nameMarkerGroup = 2;
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 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 = diagnostics.find(d => d.message.indexOf(message) >= 0) as Diagnostic;
expect(diagnostic).toBeDefined();
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);
if (len != null) {
expect(diagnostic.span.end - diagnostic.span.start).toEqual(len);
}
}
}
}

View File

@ -0,0 +1,264 @@
/**
* @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 'reflect-metadata';
import * as ts from 'typescript';
import {LanguageServicePlugin} from '../src/ts_plugin';
import {toh} from './test_data';
import {MockTypescriptHost} from './test_utils';
describe('plugin', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
it('should not report errors on tour of heroes', () => {
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
for (let source of program.getSourceFiles()) {
expectNoDiagnostics(service.getSyntacticDiagnostics(source.fileName));
expectNoDiagnostics(service.getSemanticDiagnostics(source.fileName));
}
});
let plugin =
new LanguageServicePlugin({ts: ts, host: mockHost, service, registry: documentRegistry});
it('should not report template errors on tour of heroes', () => {
for (let source of program.getSourceFiles()) {
// Ignore all 'cases.ts' files as they intentionally contain errors.
if (!source.fileName.endsWith('cases.ts')) {
expectNoDiagnostics(plugin.getSemanticDiagnosticsFilter(source.fileName, []));
}
}
});
it('should be able to get entity completions',
() => { contains('app/app.component.ts', 'entity-amp', '&amp;', '&gt;', '&lt;', '&iota;'); });
it('should be able to return html elements', () => {
let htmlTags = ['h1', 'h2', 'div', 'span'];
let locations = ['empty', 'start-tag-h1', 'h1-content', 'start-tag', 'start-tag-after-h'];
for (let location of locations) {
contains('app/app.component.ts', location, ...htmlTags);
}
});
it('should be able to return element diretives',
() => { contains('app/app.component.ts', 'empty', 'my-app'); });
it('should be able to return h1 attributes',
() => { contains('app/app.component.ts', 'h1-after-space', 'id', 'dir', 'lang', 'onclick'); });
it('should be able to find common angular attributes', () => {
contains('app/app.component.ts', 'div-attributes', '(click)', '[ngClass]', '*ngIf', '*ngFor');
});
it('should be able to returned attribute names with an incompete attribute',
() => { contains('app/parsing-cases.ts', 'no-value-attribute', 'id', 'dir', 'lang'); });
it('should be able to return attributes of an incomplete element', () => {
contains('app/parsing-cases.ts', 'incomplete-open-lt', 'a');
contains('app/parsing-cases.ts', 'incomplete-open-a', 'a');
contains('app/parsing-cases.ts', 'incomplete-open-attr', 'id', 'dir', 'lang');
});
it('should be able to return completions with a missing closing tag',
() => { contains('app/parsing-cases.ts', 'missing-closing', 'h1', 'h2'); });
it('should be able to return common attributes of in an unknown tag',
() => { contains('app/parsing-cases.ts', 'unknown-element', 'id', 'dir', 'lang'); });
it('should be able to get the completions at the beginning of an interpolation',
() => { contains('app/app.component.ts', 'h2-hero', 'hero', 'title'); });
it('should not include private members of the of a class',
() => { contains('app/app.component.ts', 'h2-hero', '-internal'); });
it('should be able to get the completions at the end of an interpolation',
() => { contains('app/app.component.ts', 'sub-end', 'hero', 'title'); });
it('should be able to get the completions in a property read',
() => { contains('app/app.component.ts', 'h2-name', 'name', 'id'); });
it('should be able to get a list of pipe values', () => {
contains('app/parsing-cases.ts', 'before-pipe', 'lowercase', 'uppercase');
contains('app/parsing-cases.ts', 'in-pipe', 'lowercase', 'uppercase');
contains('app/parsing-cases.ts', 'after-pipe', 'lowercase', 'uppercase');
});
it('should be able get completions in an empty interpolation',
() => { contains('app/parsing-cases.ts', 'empty-interpolation', 'title', 'subTitle'); });
describe('with attributes', () => {
it('should be able to complete property value',
() => { contains('app/parsing-cases.ts', 'property-binding-model', 'test'); });
it('should be able to complete an event',
() => { contains('app/parsing-cases.ts', 'event-binding-model', 'modelChanged'); });
it('should be able to complete a two-way binding',
() => { contains('app/parsing-cases.ts', 'two-way-binding-model', 'test'); });
});
describe('with a *ngFor', () => {
it('should include a let for empty attribute',
() => { contains('app/parsing-cases.ts', 'for-empty', 'let'); });
it('should not suggest any entries if in the name part of a let',
() => { expectEmpty('app/parsing-cases.ts', 'for-let-empty'); });
it('should suggest NgForRow members for let initialization expression', () => {
contains(
'app/parsing-cases.ts', 'for-let-i-equal', 'index', 'count', 'first', 'last', 'even',
'odd');
});
it('should include a let', () => { contains('app/parsing-cases.ts', 'for-let', 'let'); });
it('should include an "of"', () => { contains('app/parsing-cases.ts', 'for-of', 'of'); });
it('should include field reference',
() => { contains('app/parsing-cases.ts', 'for-people', 'people'); });
it('should include person in the let scope',
() => { contains('app/parsing-cases.ts', 'for-interp-person', 'person'); });
// TODO: Enable when we can infer the element type of the ngFor
// it('should include determine person\'s type as Person', () => {
// contains('app/parsing-cases.ts', 'for-interp-name', 'name', 'age');
// contains('app/parsing-cases.ts', 'for-interp-age', 'name', 'age');
// });
});
describe('for pipes', () => {
it('should be able to resolve lowercase',
() => { contains('app/expression-cases.ts', 'string-pipe', 'substring'); });
});
describe('with references', () => {
it('should list references',
() => { contains('app/parsing-cases.ts', 'test-comp-content', 'test1', 'test2', 'div'); });
it('should reference the component',
() => { contains('app/parsing-cases.ts', 'test-comp-after-test', 'name'); });
// TODO: Enable when we have a flag that indicates the project targets the DOM
// it('should refernce the element if no component', () => {
// contains('app/parsing-cases.ts', 'test-comp-after-div', 'innerText');
// });
});
describe('for semantic errors', () => {
it('should report access to an unknown field', () => {
expectSemanticError(
'app/expression-cases.ts', 'foo',
'Identifier \'foo\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member');
});
it('should report access to an unknown sub-field', () => {
expectSemanticError(
'app/expression-cases.ts', 'nam',
'Identifier \'nam\' is not defined. \'Person\' does not contain such a member');
});
it('should report access to a private member', () => {
expectSemanticError(
'app/expression-cases.ts', 'myField',
'Identifier \'myField\' refers to a private member of the component');
});
it('should report numeric operator erros',
() => { expectSemanticError('app/expression-cases.ts', 'mod', 'Expected a numeric type'); });
describe('in ngFor', () => {
function expectError(locationMarker: string, message: string) {
expectSemanticError('app/ng-for-cases.ts', locationMarker, message);
}
it('should report an unknown field', () => {
expectError(
'people_1',
'Identifier \'people_1\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member');
});
it('should report an unknown context reference', () => {
expectError('even_1', 'The template context does not defined a member called \'even_1\'');
});
it('should report an unknown value in a key expression', () => {
expectError(
'trackBy_1',
'Identifier \'trackBy_1\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member');
});
});
describe('in ngIf', () => {
function expectError(locationMarker: string, message: string) {
expectSemanticError('app/ng-if-cases.ts', locationMarker, message);
}
it('should report an implicit context reference', () => {
expectError('implicit', 'The template context does not have an implicit value');
});
});
});
function getMarkerLocation(fileName: string, locationMarker: string): number {
const location = mockHost.getMarkerLocations(fileName)[locationMarker];
if (location == null) {
throw new Error(`No marker ${locationMarker} found.`);
}
return location;
}
function contains(fileName: string, locationMarker: string, ...names: string[]) {
const location = getMarkerLocation(fileName, locationMarker);
expectEntries(locationMarker, plugin.getCompletionsAtPosition(fileName, location), ...names);
}
function expectEmpty(fileName: string, locationMarker: string) {
const location = getMarkerLocation(fileName, locationMarker);
expect(plugin.getCompletionsAtPosition(fileName, location).entries).toEqual([]);
}
function expectSemanticError(fileName: string, locationMarker: string, message: string) {
const start = getMarkerLocation(fileName, locationMarker);
const end = getMarkerLocation(fileName, locationMarker + '-end');
const errors = plugin.getSemanticDiagnosticsFilter(fileName, []);
for (const error of errors) {
if (error.messageText.toString().indexOf(message) >= 0) {
expect(error.start).toEqual(start);
expect(error.length).toEqual(end - start);
return;
}
}
throw new Error(
`Expected error messages to contain ${message}, in messages:\n ${errors.map(e => e.messageText.toString()).join(',\n ')}`);
}
});
function expectEntries(locationMarker: string, info: ts.CompletionInfo, ...names: string[]) {
let entries: {[name: string]: boolean} = {};
if (!info) {
throw new Error(
`Expected result from ${locationMarker} to include ${names.join(', ')} but no result provided`);
} else {
for (let entry of info.entries) {
entries[entry.name] = true;
}
let shouldContains = names.filter(name => !name.startsWith('-'));
let shouldNotContain = names.filter(name => name.startsWith('-'));
let missing = shouldContains.filter(name => !entries[name]);
let present = shouldNotContain.map(name => name.substr(1)).filter(name => entries[name]);
if (missing.length) {
throw new Error(
`Expected result from ${locationMarker} to include at least one of the following, ${missing.join(', ')}, in the list of entries ${info.entries.map(entry => entry.name).join(', ')}`);
}
if (present.length) {
throw new Error(
`Unexpected member${present.length > 1 ? 's': ''} included in result: ${present.join(', ')}`);
}
}
}
function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
for (const diagnostic of diagnostics) {
let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
if (diagnostic.start) {
let {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
} else {
console.log(`${message}`);
}
}
expect(diagnostics.length).toBe(0);
}