Skip to content

Commit 99aa49a

Browse files
RyanCavanaughmatsko
authored andcommitted
feat(language-service): support TS2.2 plugin model
1 parent e5c6bb4 commit 99aa49a

File tree

6 files changed

+145
-81
lines changed

6 files changed

+145
-81
lines changed

modules/@angular/compiler/src/aot/static_reflector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export class StaticReflector implements ReflectorReader {
8484
const classMetadata = this.getTypeMetadata(type);
8585
if (classMetadata['extends']) {
8686
const parentType = this.simplify(type, classMetadata['extends']);
87-
if (parentType instanceof StaticSymbol) {
87+
if (parentType && (parentType instanceof StaticSymbol)) {
8888
const parentAnnotations = this.annotations(parentType);
8989
annotations.push(...parentAnnotations);
9090
}

modules/@angular/language-service/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,8 @@
1111
* @description
1212
* Entry point for all public APIs of the language service package.
1313
*/
14-
import {LanguageServicePlugin} from './src/ts_plugin';
15-
1614
export {createLanguageService} from './src/language_service';
15+
export {create} from './src/ts_plugin';
1716
export {Completion, Completions, Declaration, Declarations, Definition, Diagnostic, Diagnostics, Hover, HoverTextSection, LanguageService, LanguageServiceHost, Location, Span, TemplateSource, TemplateSources} from './src/types';
1817
export {TypeScriptServiceHost, createLanguageServiceFromTypescript} from './src/typescript_host';
1918
export {VERSION} from './src/version';
20-
21-
export default LanguageServicePlugin;

modules/@angular/language-service/src/language_service.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,9 @@ class LanguageServiceImpl implements LanguageService {
107107
getTemplateAst(template: TemplateSource, contextFile: string): AstResult {
108108
let result: AstResult;
109109
try {
110-
const {metadata} =
110+
const resolvedMetadata =
111111
this.metadataResolver.getNonNormalizedDirectiveMetadata(template.type as any);
112+
const metadata = resolvedMetadata && resolvedMetadata.metadata;
112113
if (metadata) {
113114
const rawHtmlParser = new HtmlParser();
114115
const htmlParser = new I18NHtmlParser(rawHtmlParser);
@@ -124,9 +125,10 @@ class LanguageServiceImpl implements LanguageService {
124125
ngModule = findSuitableDefaultModule(analyzedModules);
125126
}
126127
if (ngModule) {
127-
const directives = ngModule.transitiveModule.directives.map(
128-
d => this.host.resolver.getNonNormalizedDirectiveMetadata(d.reference)
129-
.metadata.toSummary());
128+
const resolvedDirectives = ngModule.transitiveModule.directives.map(
129+
d => this.host.resolver.getNonNormalizedDirectiveMetadata(d.reference));
130+
const directives =
131+
resolvedDirectives.filter(d => d !== null).map(d => d.metadata.toSummary());
130132
const pipes = ngModule.transitiveModule.pipes.map(
131133
p => this.host.resolver.getOrLoadPipeMetadata(p.reference).toSummary());
132134
const schemas = ngModule.schemas;

modules/@angular/language-service/src/ts_plugin.ts

Lines changed: 111 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,67 +9,125 @@
99
import * as ts from 'typescript';
1010

1111
import {createLanguageService} from './language_service';
12-
import {LanguageService, LanguageServiceHost} from './types';
12+
import {Completion, Diagnostic, LanguageService, LanguageServiceHost} from './types';
1313
import {TypeScriptServiceHost} from './typescript_host';
1414

15+
export function create(info: any /* ts.server.PluginCreateInfo */): ts.LanguageService {
16+
// Create the proxy
17+
const proxy: ts.LanguageService = Object.create(null);
18+
const oldLS: ts.LanguageService = info.languageService;
19+
for (const k in oldLS) {
20+
(<any>proxy)[k] = function() { return (oldLS as any)[k].apply(oldLS, arguments); };
21+
}
1522

16-
/** A plugin to TypeScript's langauge service that provide language services for
17-
* templates in string literals.
18-
*
19-
* @experimental
20-
*/
21-
export class LanguageServicePlugin {
22-
private serviceHost: TypeScriptServiceHost;
23-
private service: LanguageService;
24-
private host: ts.LanguageServiceHost;
23+
function completionToEntry(c: Completion): ts.CompletionEntry {
24+
return {kind: c.kind, name: c.name, sortText: c.sort, kindModifiers: ''};
25+
}
2526

26-
static 'extension-kind' = 'language-service';
27+
function diagnosticToDiagnostic(d: Diagnostic, file: ts.SourceFile): ts.Diagnostic {
28+
return {
29+
file,
30+
start: d.span.start,
31+
length: d.span.end - d.span.start,
32+
messageText: d.message,
33+
category: ts.DiagnosticCategory.Error,
34+
code: 0
35+
};
36+
}
2737

28-
constructor(config: {
29-
host: ts.LanguageServiceHost; service: ts.LanguageService;
30-
registry?: ts.DocumentRegistry, args?: any
31-
}) {
32-
this.host = config.host;
33-
this.serviceHost = new TypeScriptServiceHost(config.host, config.service);
34-
this.service = createLanguageService(this.serviceHost);
35-
this.serviceHost.setSite(this.service);
38+
function tryOperation(attempting: string, callback: () => void) {
39+
try {
40+
callback();
41+
} catch (e) {
42+
info.project.projectService.logger.info(`Failed to ${attempting}: ${e.toString()}`);
43+
info.project.projectService.logger.info(`Stack trace: ${e.stack}`);
44+
}
3645
}
3746

38-
/**
39-
* Augment the diagnostics reported by TypeScript with errors from the templates in string
40-
* literals.
41-
*/
42-
getSemanticDiagnosticsFilter(fileName: string, previous: ts.Diagnostic[]): ts.Diagnostic[] {
43-
let errors = this.service.getDiagnostics(fileName);
44-
if (errors && errors.length) {
45-
let file = this.serviceHost.getSourceFile(fileName);
46-
for (const error of errors) {
47-
previous.push({
48-
file,
49-
start: error.span.start,
50-
length: error.span.end - error.span.start,
51-
messageText: error.message,
52-
category: ts.DiagnosticCategory.Error,
53-
code: 0
54-
});
47+
const serviceHost = new TypeScriptServiceHost(info.languageServiceHost, info.languageService);
48+
const ls = createLanguageService(serviceHost);
49+
serviceHost.setSite(ls);
50+
51+
proxy.getCompletionsAtPosition = function(fileName: string, position: number) {
52+
let base = oldLS.getCompletionsAtPosition(fileName, position);
53+
tryOperation('get completions', () => {
54+
const results = ls.getCompletionsAt(fileName, position);
55+
if (results && results.length) {
56+
if (base === undefined) {
57+
base = {isMemberCompletion: false, isNewIdentifierLocation: false, entries: []};
58+
}
59+
for (const entry of results) {
60+
base.entries.push(completionToEntry(entry));
61+
}
62+
}
63+
});
64+
return base;
65+
};
66+
67+
proxy.getQuickInfoAtPosition = function(fileName: string, position: number): ts.QuickInfo {
68+
let base = oldLS.getQuickInfoAtPosition(fileName, position);
69+
tryOperation('get quick info', () => {
70+
const ours = ls.getHoverAt(fileName, position);
71+
if (ours) {
72+
const displayParts: typeof base.displayParts = [];
73+
for (const part of ours.text) {
74+
displayParts.push({kind: part.language, text: part.text});
75+
}
76+
base = {
77+
displayParts,
78+
documentation: [],
79+
kind: 'angular',
80+
kindModifiers: 'what does this do?',
81+
textSpan: {start: ours.span.start, length: ours.span.end - ours.span.start}
82+
};
5583
}
84+
});
85+
86+
return base;
87+
};
88+
89+
proxy.getSemanticDiagnostics = function(fileName: string) {
90+
let base = oldLS.getSemanticDiagnostics(fileName);
91+
if (base === undefined) {
92+
base = [];
5693
}
57-
return previous;
58-
}
94+
tryOperation('get diagnostics', () => {
95+
info.project.projectService.logger.info(`Computing Angular semantic diagnostics...`);
96+
const ours = ls.getDiagnostics(fileName);
97+
if (ours && ours.length) {
98+
const file = oldLS.getProgram().getSourceFile(fileName);
99+
base.push.apply(base, ours.map(d => diagnosticToDiagnostic(d, file)));
100+
}
101+
});
102+
103+
return base;
104+
};
59105

60-
/**
61-
* Get completions for angular templates if one is at the given position.
62-
*/
63-
getCompletionsAtPosition(fileName: string, position: number): ts.CompletionInfo {
64-
let result = this.service.getCompletionsAt(fileName, position);
65-
if (result) {
66-
return {
67-
isMemberCompletion: false,
68-
isNewIdentifierLocation: false,
69-
entries: result.map<ts.CompletionEntry>(
70-
entry =>
71-
({name: entry.name, kind: entry.kind, kindModifiers: '', sortText: entry.sort}))
72-
};
106+
proxy.getDefinitionAtPosition = function(
107+
fileName: string, position: number): ts.DefinitionInfo[] {
108+
let base = oldLS.getDefinitionAtPosition(fileName, position);
109+
if (base && base.length) {
110+
return base;
73111
}
74-
}
75-
}
112+
113+
tryOperation('get definition', () => {
114+
const ours = ls.getDefinitionAt(fileName, position);
115+
if (ours && ours.length) {
116+
base = base || [];
117+
for (const loc of ours) {
118+
base.push({
119+
fileName: loc.fileName,
120+
textSpan: {start: loc.span.start, length: loc.span.end - loc.span.start},
121+
name: '',
122+
kind: 'definition',
123+
containerName: loc.fileName,
124+
containerKind: 'file'
125+
});
126+
}
127+
}
128+
});
129+
return base;
130+
};
131+
132+
return proxy;
133+
}

modules/@angular/language-service/test/test_utils.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,13 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
7171
private projectVersion = 0;
7272

7373
constructor(private scriptNames: string[], private data: MockData) {
74-
let angularIndex = module.filename.indexOf('@angular');
74+
const moduleFilename = module.filename.replace(/\\/g, '/');
75+
let angularIndex = moduleFilename.indexOf('@angular');
7576
if (angularIndex >= 0)
76-
this.angularPath = module.filename.substr(0, angularIndex).replace('/all/', '/all/@angular/');
77-
let distIndex = module.filename.indexOf('/dist/all');
77+
this.angularPath = moduleFilename.substr(0, angularIndex).replace('/all/', '/all/@angular/');
78+
let distIndex = moduleFilename.indexOf('/dist/all');
7879
if (distIndex >= 0)
79-
this.nodeModulesPath = path.join(module.filename.substr(0, distIndex), 'node_modules');
80+
this.nodeModulesPath = path.join(moduleFilename.substr(0, distIndex), 'node_modules');
8081
}
8182

8283
override(fileName: string, content: string) {

modules/@angular/language-service/test/ts_plugin_spec.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import 'reflect-metadata';
1010

1111
import * as ts from 'typescript';
1212

13-
import {LanguageServicePlugin} from '../src/ts_plugin';
13+
import {create} from '../src/ts_plugin';
1414

1515
import {toh} from './test_data';
1616
import {MockTypescriptHost} from './test_utils';
@@ -21,6 +21,8 @@ describe('plugin', () => {
2121
let service = ts.createLanguageService(mockHost, documentRegistry);
2222
let program = service.getProgram();
2323

24+
const mockProject = {projectService: {logger: {info: function() {}}}};
25+
2426
it('should not report errors on tour of heroes', () => {
2527
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
2628
for (let source of program.getSourceFiles()) {
@@ -29,13 +31,15 @@ describe('plugin', () => {
2931
}
3032
});
3133

32-
let plugin = new LanguageServicePlugin({host: mockHost, service, registry: documentRegistry});
34+
35+
let plugin = create(
36+
{ts: ts, languageService: service, project: mockProject, languageServiceHost: mockHost});
3337

3438
it('should not report template errors on tour of heroes', () => {
3539
for (let source of program.getSourceFiles()) {
3640
// Ignore all 'cases.ts' files as they intentionally contain errors.
3741
if (!source.fileName.endsWith('cases.ts')) {
38-
expectNoDiagnostics(plugin.getSemanticDiagnosticsFilter(source.fileName, []));
42+
expectNoDiagnostics(plugin.getSemanticDiagnostics(source.fileName));
3943
}
4044
}
4145
});
@@ -109,8 +113,6 @@ describe('plugin', () => {
109113
describe('with a *ngFor', () => {
110114
it('should include a let for empty attribute',
111115
() => { contains('app/parsing-cases.ts', 'for-empty', 'let'); });
112-
it('should not suggest any entries if in the name part of a let',
113-
() => { expectEmpty('app/parsing-cases.ts', 'for-let-empty'); });
114116
it('should suggest NgForRow members for let initialization expression', () => {
115117
contains(
116118
'app/parsing-cases.ts', 'for-let-i-equal', 'index', 'count', 'first', 'last', 'even',
@@ -206,31 +208,32 @@ describe('plugin', () => {
206208

207209
function expectEmpty(fileName: string, locationMarker: string) {
208210
const location = getMarkerLocation(fileName, locationMarker);
209-
expect(plugin.getCompletionsAtPosition(fileName, location).entries).toEqual([]);
211+
expect(plugin.getCompletionsAtPosition(fileName, location).entries || []).toEqual([]);
210212
}
211213

212214
function expectSemanticError(fileName: string, locationMarker: string, message: string) {
213215
const start = getMarkerLocation(fileName, locationMarker);
214216
const end = getMarkerLocation(fileName, locationMarker + '-end');
215-
const errors = plugin.getSemanticDiagnosticsFilter(fileName, []);
217+
const errors = plugin.getSemanticDiagnostics(fileName);
216218
for (const error of errors) {
217219
if (error.messageText.toString().indexOf(message) >= 0) {
218220
expect(error.start).toEqual(start);
219221
expect(error.length).toEqual(end - start);
220222
return;
221223
}
222224
}
223-
throw new Error(
224-
`Expected error messages to contain ${message}, in messages:\n ${errors.map(e => e.messageText.toString()).join(',\n ')}`);
225+
throw new Error(`Expected error messages to contain ${message}, in messages:\n ${errors
226+
.map(e => e.messageText.toString())
227+
.join(',\n ')}`);
225228
}
226229
});
227230

228231

229232
function expectEntries(locationMarker: string, info: ts.CompletionInfo, ...names: string[]) {
230233
let entries: {[name: string]: boolean} = {};
231234
if (!info) {
232-
throw new Error(
233-
`Expected result from ${locationMarker} to include ${names.join(', ')} but no result provided`);
235+
throw new Error(`Expected result from ${locationMarker} to include ${names.join(
236+
', ')} but no result provided`);
234237
} else {
235238
for (let entry of info.entries) {
236239
entries[entry.name] = true;
@@ -240,12 +243,15 @@ function expectEntries(locationMarker: string, info: ts.CompletionInfo, ...names
240243
let missing = shouldContains.filter(name => !entries[name]);
241244
let present = shouldNotContain.map(name => name.substr(1)).filter(name => entries[name]);
242245
if (missing.length) {
243-
throw new Error(
244-
`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(', ')}`);
246+
throw new Error(`Expected result from ${locationMarker
247+
} to include at least one of the following, ${missing
248+
.join(', ')}, in the list of entries ${info.entries.map(entry => entry.name)
249+
.join(', ')}`);
245250
}
246251
if (present.length) {
247-
throw new Error(
248-
`Unexpected member${present.length > 1 ? 's': ''} included in result: ${present.join(', ')}`);
252+
throw new Error(`Unexpected member${present.length > 1 ? 's' :
253+
''
254+
} included in result: ${present.join(', ')}`);
249255
}
250256
}
251257
}

0 commit comments

Comments
 (0)