-
Notifications
You must be signed in to change notification settings - Fork 24.8k
/
ts_plugin_spec.ts
303 lines (270 loc) Β· 13.4 KB
/
ts_plugin_spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/**
* @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 {create} from '../src/ts_plugin';
import {toh} from './test_data';
import {MockTypescriptHost} from './test_utils';
describe('plugin', () => {
const mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
const service = ts.createLanguageService(mockHost);
const program = service.getProgram();
const plugin = createPlugin(service, mockHost);
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));
}
});
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.getSemanticDiagnostics(source.fileName));
}
}
});
it('should be able to get entity completions',
() => { contains('app/app.component.ts', '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/app.component.ts', location, ...htmlTags);
}
});
it('should be able to return element directives',
() => { 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 return 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 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 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',
() => { 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 to 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 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 reference 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 errors',
() => { 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 define 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 define a member called \'unknown\'');
});
});
describe(`with config 'angularOnly = true`, () => {
const ngLS = createPlugin(service, mockHost, {angularOnly: true});
it('should not report template errors on TOH', () => {
const sourceFiles = ngLS.getProgram() !.getSourceFiles();
expect(sourceFiles.length).toBeGreaterThan(0);
for (const {fileName} of sourceFiles) {
// Ignore all 'cases.ts' files as they intentionally contain errors.
if (!fileName.endsWith('cases.ts')) {
expectNoDiagnostics(ngLS.getSemanticDiagnostics(fileName));
}
}
});
it('should be able to get entity completions', () => {
const fileName = 'app/app.component.ts';
const marker = 'entity-amp';
const position = getMarkerLocation(fileName, marker);
const results = ngLS.getCompletionsAtPosition(fileName, position, {} /* options */);
expect(results).toBeTruthy();
expectEntries(marker, results !, ...['&', '>', '<', 'ι']);
});
it('should report template diagnostics', () => {
// TODO(kyliau): Rename these to end with '-error.ts'
const fileName = 'app/expression-cases.ts';
const diagnostics = ngLS.getSemanticDiagnostics(fileName);
expect(diagnostics.map(d => d.messageText)).toEqual([
`Identifier 'foo' is not defined. The component declaration, template variable declarations, and element references do not contain such a member`,
`Identifier 'nam' is not defined. 'Person' does not contain such a member`,
`Identifier 'myField' refers to a private member of the component`,
`Expected a numeric type`,
]);
});
});
});
function createPlugin(tsLS: ts.LanguageService, tsLSHost: ts.LanguageServiceHost, config = {}) {
const project = {projectService: {logger: {info() {}}}};
return create({
languageService: tsLS,
languageServiceHost: tsLSHost,
project: project as any,
serverHost: {} as any,
config: {...config},
});
}
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, undefined) !, ...names);
}
function expectSemanticError(fileName: string, locationMarker: string, message: string) {
const start = getMarkerLocation(fileName, locationMarker);
const end = getMarkerLocation(fileName, locationMarker + '-end');
const errors = plugin.getSemanticDiagnostics(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.file && diagnostic.start) {
let {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
console.error(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
} else {
console.error(`${message}`);
}
}
expect(diagnostics.length).toBe(0);
}