diff --git a/packages/language-service/src/diagnostics.ts b/packages/language-service/src/diagnostics.ts index 2ee1e31841..b3ba76546f 100644 --- a/packages/language-service/src/diagnostics.ts +++ b/packages/language-service/src/diagnostics.ts @@ -7,49 +7,13 @@ */ import {NgAnalyzedModules, StaticSymbol} from '@angular/compiler'; -import {DiagnosticTemplateInfo, getTemplateExpressionDiagnostics} from '@angular/compiler-cli/src/language_services'; - import {AstResult} from './common'; import {Declarations, Diagnostic, DiagnosticKind, DiagnosticMessageChain, Diagnostics, Span, TemplateSource} from './types'; -import {offsetSpan, spanOf} from './utils'; export interface AstProvider { getTemplateAst(template: TemplateSource, fileName: string): AstResult; } -export function getTemplateDiagnostics( - fileName: string, astProvider: AstProvider, templates: TemplateSource[]): Diagnostics { - const results: Diagnostics = []; - for (const template of templates) { - const ast = astProvider.getTemplateAst(template, fileName); - if (ast) { - if (ast.parseErrors && ast.parseErrors.length) { - results.push(...ast.parseErrors.map( - e => ({ - kind: DiagnosticKind.Error, - span: offsetSpan(spanOf(e.span), template.span.start), - message: e.msg - }))); - } else if (ast.templateAst && ast.htmlAst) { - const info: DiagnosticTemplateInfo = { - templateAst: ast.templateAst, - htmlAst: ast.htmlAst, - offset: template.span.start, - query: template.query, - members: template.members - }; - const expressionDiagnostics = getTemplateExpressionDiagnostics(info); - results.push(...expressionDiagnostics); - } - if (ast.errors) { - results.push(...ast.errors.map( - e => ({kind: e.kind, span: e.span || template.span, message: e.message}))); - } - } - } - return results; -} - export function getDeclarationDiagnostics( declarations: Declarations, modules: NgAnalyzedModules): Diagnostics { const results: Diagnostics = []; diff --git a/packages/language-service/src/language_service.ts b/packages/language-service/src/language_service.ts index 94e0bcc56c..fb2754eda2 100644 --- a/packages/language-service/src/language_service.ts +++ b/packages/language-service/src/language_service.ts @@ -6,14 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileMetadataResolver, CompileNgModuleMetadata, CompilePipeSummary, CompilerConfig, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgAnalyzedModules, Parser, TemplateParser} from '@angular/compiler'; +import {CompileMetadataResolver, CompilePipeSummary} from '@angular/compiler'; +import {DiagnosticTemplateInfo, getTemplateExpressionDiagnostics} from '@angular/compiler-cli/src/language_services'; -import {AstResult, TemplateInfo} from './common'; import {getTemplateCompletions} from './completions'; import {getDefinition} from './definitions'; -import {getDeclarationDiagnostics, getTemplateDiagnostics} from './diagnostics'; +import {getDeclarationDiagnostics} from './diagnostics'; import {getHover} from './hover'; import {Completions, Definition, Diagnostic, DiagnosticKind, Diagnostics, Hover, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types'; +import {offsetSpan, spanOf} from './utils'; + /** @@ -36,7 +38,7 @@ class LanguageServiceImpl implements LanguageService { let results: Diagnostics = []; let templates = this.host.getTemplates(fileName); if (templates && templates.length) { - results.push(...getTemplateDiagnostics(fileName, this, templates)); + results.push(...this.getTemplateDiagnostics(fileName, templates)); } let declarations = this.host.getDeclarations(fileName); @@ -49,7 +51,7 @@ class LanguageServiceImpl implements LanguageService { } getPipesAt(fileName: string, position: number): CompilePipeSummary[] { - let templateInfo = this.getTemplateAstAtPosition(fileName, position); + let templateInfo = this.host.getTemplateAstAtPosition(fileName, position); if (templateInfo) { return templateInfo.pipes; } @@ -57,100 +59,59 @@ class LanguageServiceImpl implements LanguageService { } getCompletionsAt(fileName: string, position: number): Completions { - let templateInfo = this.getTemplateAstAtPosition(fileName, position); + let templateInfo = this.host.getTemplateAstAtPosition(fileName, position); if (templateInfo) { return getTemplateCompletions(templateInfo); } } getDefinitionAt(fileName: string, position: number): Definition { - let templateInfo = this.getTemplateAstAtPosition(fileName, position); + let templateInfo = this.host.getTemplateAstAtPosition(fileName, position); if (templateInfo) { return getDefinition(templateInfo); } } getHoverAt(fileName: string, position: number): Hover|undefined { - let templateInfo = this.getTemplateAstAtPosition(fileName, position); + let templateInfo = this.host.getTemplateAstAtPosition(fileName, position); if (templateInfo) { return getHover(templateInfo); } } - private getTemplateAstAtPosition(fileName: string, position: number): TemplateInfo|undefined { - let template = this.host.getTemplateAt(fileName, position); - if (template) { - let astResult = this.getTemplateAst(template, fileName); - if (astResult && astResult.htmlAst && astResult.templateAst && astResult.directive && - astResult.directives && astResult.pipes && astResult.expressionParser) - return { - position, - fileName, - template, - htmlAst: astResult.htmlAst, - directive: astResult.directive, - directives: astResult.directives, - pipes: astResult.pipes, - templateAst: astResult.templateAst, - expressionParser: astResult.expressionParser - }; - } - return undefined; - } - - getTemplateAst(template: TemplateSource, contextFile: string): AstResult { - let result: AstResult|undefined = undefined; - try { - const resolvedMetadata = - this.metadataResolver.getNonNormalizedDirectiveMetadata(template.type as any); - const metadata = resolvedMetadata && resolvedMetadata.metadata; - if (metadata) { - const rawHtmlParser = new HtmlParser(); - const htmlParser = new I18NHtmlParser(rawHtmlParser); - const expressionParser = new Parser(new Lexer()); - const config = new CompilerConfig(); - const parser = new TemplateParser( - config, this.host.resolver.getReflector(), expressionParser, - new DomElementSchemaRegistry(), htmlParser, null !, []); - const htmlResult = htmlParser.parse(template.source, '', {tokenizeExpansionForms: true}); - const analyzedModules = this.host.getAnalyzedModules(); - let errors: Diagnostic[]|undefined = undefined; - let ngModule = analyzedModules.ngModuleByPipeOrDirective.get(template.type); - if (!ngModule) { - // Reported by the the declaration diagnostics. - ngModule = findSuitableDefaultModule(analyzedModules); - } - if (ngModule) { - const resolvedDirectives = ngModule.transitiveModule.directives.map( - d => this.host.resolver.getNonNormalizedDirectiveMetadata(d.reference)); - const directives = removeMissing(resolvedDirectives).map(d => d.metadata.toSummary()); - const pipes = ngModule.transitiveModule.pipes.map( - p => this.host.resolver.getOrLoadPipeMetadata(p.reference).toSummary()); - const schemas = ngModule.schemas; - const parseResult = parser.tryParseHtml(htmlResult, metadata, directives, pipes, schemas); - result = { - htmlAst: htmlResult.rootNodes, - templateAst: parseResult.templateAst, - directive: metadata, directives, pipes, - parseErrors: parseResult.errors, expressionParser, errors + private getTemplateDiagnostics(fileName: string, templates: TemplateSource[]): Diagnostics { + const results: Diagnostics = []; + for (const template of templates) { + const ast = this.host.getTemplateAst(template, fileName); + if (ast) { + if (ast.parseErrors && ast.parseErrors.length) { + results.push(...ast.parseErrors.map( + e => ({ + kind: DiagnosticKind.Error, + span: offsetSpan(spanOf(e.span), template.span.start), + message: e.msg + }))); + } else if (ast.templateAst && ast.htmlAst) { + const info: DiagnosticTemplateInfo = { + templateAst: ast.templateAst, + htmlAst: ast.htmlAst, + offset: template.span.start, + query: template.query, + members: template.members }; + const expressionDiagnostics = getTemplateExpressionDiagnostics(info); + results.push(...expressionDiagnostics); + } + if (ast.errors) { + results.push(...ast.errors.map( + e => ({kind: e.kind, span: e.span || template.span, message: e.message}))); } } - } catch (e) { - let span = template.span; - if (e.fileName == contextFile) { - span = template.query.getSpanAt(e.line, e.column) || span; - } - result = {errors: [{kind: DiagnosticKind.Error, message: e.message, span}]}; } - return result || {}; + return results; } } -function removeMissing(values: (T | null | undefined)[]): T[] { - return values.filter(e => !!e) as T[]; -} - function uniqueBySpan < T extends { span: Span; } @@ -173,16 +134,3 @@ function uniqueBySpan < T extends { return result; } } - -function findSuitableDefaultModule(modules: NgAnalyzedModules): CompileNgModuleMetadata|undefined { - let result: CompileNgModuleMetadata|undefined = undefined; - let resultSize = 0; - for (const module of modules.ngModules) { - const moduleSize = module.transitiveModule.directives.length; - if (moduleSize > resultSize) { - result = module; - resultSize = moduleSize; - } - } - return result; -} diff --git a/packages/language-service/src/ts_plugin.ts b/packages/language-service/src/ts_plugin.ts index 434a928aae..e89de93f38 100644 --- a/packages/language-service/src/ts_plugin.ts +++ b/packages/language-service/src/ts_plugin.ts @@ -81,7 +81,6 @@ export function create(info: tss.server.PluginCreateInfo): ts.LanguageService { const serviceHost = new TypeScriptServiceHost(info.languageServiceHost, oldLS); const ls = createLanguageService(serviceHost); - serviceHost.setSite(ls); projectHostMap.set(info.project, serviceHost); proxy.getCompletionsAtPosition = function( diff --git a/packages/language-service/src/types.ts b/packages/language-service/src/types.ts index d12d836903..39ef7ab12a 100644 --- a/packages/language-service/src/types.ts +++ b/packages/language-service/src/types.ts @@ -8,6 +8,7 @@ import {CompileDirectiveMetadata, CompileMetadataResolver, CompilePipeSummary, NgAnalyzedModules, StaticSymbol} from '@angular/compiler'; import {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from '@angular/compiler-cli/src/language_services'; +import {AstResult, TemplateInfo} from './common'; export { BuiltinType, @@ -203,6 +204,16 @@ export interface LanguageServiceHost { * Return a list all the template files referenced by the project. */ getTemplateReferences(): string[]; + + /** + * Return the AST for both HTML and template for the contextFile. + */ + getTemplateAst(template: TemplateSource, contextFile: string): AstResult; + + /** + * Return the template AST for the node that corresponds to the position. + */ + getTemplateAstAtPosition(fileName: string, position: number): TemplateInfo|undefined; } /** diff --git a/packages/language-service/src/typescript_host.ts b/packages/language-service/src/typescript_host.ts index a3fa6eeaeb..a743c672f6 100644 --- a/packages/language-service/src/typescript_host.ts +++ b/packages/language-service/src/typescript_host.ts @@ -6,16 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, FormattedError, FormattedMessageChain, HtmlParser, JitSummaryResolver, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, analyzeNgModules, createOfflineCompileUrlResolver, isFormattedError} from '@angular/compiler'; +import {AotSummaryResolver, CompileMetadataResolver, CompileNgModuleMetadata, CompilePipeSummary, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, FormattedError, FormattedMessageChain, HtmlParser, I18NHtmlParser, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver, isFormattedError} from '@angular/compiler'; import {CompilerOptions, getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@angular/compiler-cli/src/language_services'; import {ViewEncapsulation, ɵConsole as Console} from '@angular/core'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; +import {AstResult, TemplateInfo} from './common'; import {createLanguageService} from './language_service'; import {ReflectorHost} from './reflector_host'; -import {Declaration, DeclarationError, Declarations, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, Symbol, SymbolQuery, TemplateSource, TemplateSources} from './types'; +import {Declaration, DeclarationError, Declarations, Diagnostic, DiagnosticKind, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, Symbol, SymbolQuery, TemplateSource, TemplateSources} from './types'; + /** @@ -25,7 +27,6 @@ export function createLanguageServiceFromTypescript( host: ts.LanguageServiceHost, service: ts.LanguageService): LanguageService { const ngHost = new TypeScriptServiceHost(host, service); const ngServer = createLanguageService(ngHost); - ngHost.setSite(ngServer); return ngServer; } @@ -75,8 +76,6 @@ export class TypeScriptServiceHost implements LanguageServiceHost { // TODO(issue/24571): remove '!'. private analyzedModules !: NgAnalyzedModules | null; // TODO(issue/24571): remove '!'. - private service !: LanguageService; - // TODO(issue/24571): remove '!'. private fileToComponent !: Map| null; // TODO(issue/24571): remove '!'. private templateReferences !: string[] | null; @@ -86,8 +85,6 @@ export class TypeScriptServiceHost implements LanguageServiceHost { constructor(private host: ts.LanguageServiceHost, private tsService: ts.LanguageService) {} - setSite(service: LanguageService) { this.service = service; } - /** * Angular LanguageServiceHost implementation */ @@ -323,7 +320,11 @@ export class TypeScriptServiceHost implements LanguageServiceHost { }, get query() { if (!queryCache) { - const pipes = t.service.getPipesAt(fileName, node.getStart()); + let pipes: CompilePipeSummary[] = []; + const templateInfo = t.getTemplateAstAtPosition(fileName, node.getStart()); + if (templateInfo) { + pipes = templateInfo.pipes; + } queryCache = getSymbolQuery( t.program !, t.checker, sourceFile, () => getPipesTable(sourceFile, t.program !, t.checker, pipes)); @@ -590,8 +591,91 @@ export class TypeScriptServiceHost implements LanguageServiceHost { return find(sourceFile); } + + getTemplateAstAtPosition(fileName: string, position: number): TemplateInfo|undefined { + let template = this.getTemplateAt(fileName, position); + if (template) { + let astResult = this.getTemplateAst(template, fileName); + if (astResult && astResult.htmlAst && astResult.templateAst && astResult.directive && + astResult.directives && astResult.pipes && astResult.expressionParser) + return { + position, + fileName, + template, + htmlAst: astResult.htmlAst, + directive: astResult.directive, + directives: astResult.directives, + pipes: astResult.pipes, + templateAst: astResult.templateAst, + expressionParser: astResult.expressionParser + }; + } + return undefined; + } + + getTemplateAst(template: TemplateSource, contextFile: string): AstResult { + let result: AstResult|undefined = undefined; + try { + const resolvedMetadata = + this.resolver.getNonNormalizedDirectiveMetadata(template.type as any); + const metadata = resolvedMetadata && resolvedMetadata.metadata; + if (metadata) { + const rawHtmlParser = new HtmlParser(); + const htmlParser = new I18NHtmlParser(rawHtmlParser); + const expressionParser = new Parser(new Lexer()); + const config = new CompilerConfig(); + const parser = new TemplateParser( + config, this.resolver.getReflector(), expressionParser, new DomElementSchemaRegistry(), + htmlParser, null !, []); + const htmlResult = htmlParser.parse(template.source, '', {tokenizeExpansionForms: true}); + const analyzedModules = this.getAnalyzedModules(); + let errors: Diagnostic[]|undefined = undefined; + let ngModule = analyzedModules.ngModuleByPipeOrDirective.get(template.type); + if (!ngModule) { + // Reported by the the declaration diagnostics. + ngModule = findSuitableDefaultModule(analyzedModules); + } + if (ngModule) { + const directives = + ngModule.transitiveModule.directives + .map(d => this.resolver.getNonNormalizedDirectiveMetadata(d.reference)) + .filter(d => d) + .map(d => d !.metadata.toSummary()); + const pipes = ngModule.transitiveModule.pipes.map( + p => this.resolver.getOrLoadPipeMetadata(p.reference).toSummary()); + const schemas = ngModule.schemas; + const parseResult = parser.tryParseHtml(htmlResult, metadata, directives, pipes, schemas); + result = { + htmlAst: htmlResult.rootNodes, + templateAst: parseResult.templateAst, + directive: metadata, directives, pipes, + parseErrors: parseResult.errors, expressionParser, errors + }; + } + } + } catch (e) { + let span = template.span; + if (e.fileName == contextFile) { + span = template.query.getSpanAt(e.line, e.column) || span; + } + result = {errors: [{kind: DiagnosticKind.Error, message: e.message, span}]}; + } + return result || {}; + } } +function findSuitableDefaultModule(modules: NgAnalyzedModules): CompileNgModuleMetadata|undefined { + let result: CompileNgModuleMetadata|undefined = undefined; + let resultSize = 0; + for (const module of modules.ngModules) { + const moduleSize = module.transitiveModule.directives.length; + if (moduleSize > resultSize) { + result = module; + resultSize = moduleSize; + } + } + return result; +} function findTsConfig(fileName: string): string|undefined { let dir = path.dirname(fileName); diff --git a/packages/language-service/test/completions_spec.ts b/packages/language-service/test/completions_spec.ts index d54624b7c9..ca06e51c18 100644 --- a/packages/language-service/test/completions_spec.ts +++ b/packages/language-service/test/completions_spec.ts @@ -22,7 +22,6 @@ describe('completions', () => { let service = ts.createLanguageService(mockHost, documentRegistry); let ngHost = new TypeScriptServiceHost(mockHost, service); let ngService = createLanguageService(ngHost); - ngHost.setSite(ngService); it('should be able to get entity completions', () => { contains('/app/test.ng', 'entity-amp', '&', '>', '<', 'ι'); }); diff --git a/packages/language-service/test/definitions_spec.ts b/packages/language-service/test/definitions_spec.ts index b7d78e1404..752d0a0ba6 100644 --- a/packages/language-service/test/definitions_spec.ts +++ b/packages/language-service/test/definitions_spec.ts @@ -22,7 +22,6 @@ describe('definitions', () => { let service = ts.createLanguageService(mockHost, documentRegistry); let ngHost = new TypeScriptServiceHost(mockHost, service); let ngService = createLanguageService(ngHost); - ngHost.setSite(ngService); it('should be able to find field in an interpolation', () => { localReference( diff --git a/packages/language-service/test/diagnostics_spec.ts b/packages/language-service/test/diagnostics_spec.ts index 892972034b..5d5178b45e 100644 --- a/packages/language-service/test/diagnostics_spec.ts +++ b/packages/language-service/test/diagnostics_spec.ts @@ -26,7 +26,6 @@ describe('diagnostics', () => { const service = ts.createLanguageService(mockHost, documentRegistry); ngHost = new TypeScriptServiceHost(mockHost, service); ngService = createLanguageService(ngHost); - ngHost.setSite(ngService); }); it('should be no diagnostics for test.ng', diff --git a/packages/language-service/test/hover_spec.ts b/packages/language-service/test/hover_spec.ts index c8a1140b09..7e0d3ffdab 100644 --- a/packages/language-service/test/hover_spec.ts +++ b/packages/language-service/test/hover_spec.ts @@ -23,7 +23,6 @@ describe('hover', () => { let service = ts.createLanguageService(mockHost, documentRegistry); let ngHost = new TypeScriptServiceHost(mockHost, service); let ngService = createLanguageService(ngHost); - ngHost.setSite(ngService); it('should be able to find field in an interpolation', () => { diff --git a/packages/language-service/test/template_references_spec.ts b/packages/language-service/test/template_references_spec.ts index 1147bc48d1..952dadb549 100644 --- a/packages/language-service/test/template_references_spec.ts +++ b/packages/language-service/test/template_references_spec.ts @@ -27,7 +27,6 @@ describe('references', () => { service = ts.createLanguageService(mockHost, documentRegistry); ngHost = new TypeScriptServiceHost(mockHost, service); ngService = createLanguageService(ngHost); - ngHost.setSite(ngService); }); it('should be able to get template references', @@ -53,4 +52,4 @@ describe('references', () => { expect(() => { ngService.getTemplateReferences(); }).not.toThrow(); }); -}); \ No newline at end of file +});