diff --git a/packages/language-service/src/diagnostics.ts b/packages/language-service/src/diagnostics.ts index d33cf5e853..fffaa88645 100644 --- a/packages/language-service/src/diagnostics.ts +++ b/packages/language-service/src/diagnostics.ts @@ -6,42 +6,43 @@ * found in the LICENSE file at https://angular.io/license */ -import {NgAnalyzedModules, StaticSymbol} from '@angular/compiler'; -import {DiagnosticTemplateInfo, getTemplateExpressionDiagnostics} from '@angular/compiler-cli/src/language_services'; +import {NgAnalyzedModules} from '@angular/compiler'; +import {getTemplateExpressionDiagnostics} from '@angular/compiler-cli/src/language_services'; import * as ts from 'typescript'; import {AstResult} from './common'; -import {Declarations, Diagnostic, DiagnosticKind, DiagnosticMessageChain, Diagnostics, Span, TemplateSource} from './types'; +import * as ng from './types'; import {offsetSpan, spanOf} from './utils'; -export interface AstProvider { - getTemplateAst(template: TemplateSource, fileName: string): AstResult; -} - -export function getTemplateDiagnostics(template: TemplateSource, ast: AstResult): Diagnostics { - const results: Diagnostics = []; +/** + * Return diagnostic information for the parsed AST of the template. + * @param template source of the template and class information + * @param ast contains HTML and template AST + */ +export function getTemplateDiagnostics( + template: ng.TemplateSource, ast: AstResult): ng.Diagnostic[] { + const results: ng.Diagnostic[] = []; if (ast.parseErrors && ast.parseErrors.length) { - results.push(...ast.parseErrors.map(e => { + results.push(...ast.parseErrors.map(e => { return { - kind: DiagnosticKind.Error, + kind: ng.DiagnosticKind.Error, span: offsetSpan(spanOf(e.span), template.span.start), message: e.msg, }; })); } else if (ast.templateAst && ast.htmlAst) { - const info: DiagnosticTemplateInfo = { + const expressionDiagnostics = getTemplateExpressionDiagnostics({ 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 => { + results.push(...ast.errors.map(e => { return { kind: e.kind, span: e.span || template.span, @@ -53,85 +54,111 @@ export function getTemplateDiagnostics(template: TemplateSource, ast: AstResult) return results; } -export function getDeclarationDiagnostics( - declarations: Declarations, modules: NgAnalyzedModules): Diagnostics { - const results: Diagnostics = []; +/** + * Generate an error message that indicates a directive is not part of any + * NgModule. + * @param name class name + * @param isComponent true if directive is an Angular Component + */ +function missingDirective(name: string, isComponent: boolean) { + const type = isComponent ? 'Component' : 'Directive'; + return `${type} '${name}' is not included in a module and will not be ` + + 'available inside a template. Consider adding it to a NgModule declaration.'; +} - let directives: Set|undefined = undefined; - for (const declaration of declarations) { - const report = (message: string | DiagnosticMessageChain, span?: Span) => { - results.push({ - kind: DiagnosticKind.Error, - span: span || declaration.declarationSpan, message - }); - }; - for (const error of declaration.errors) { - report(error.message, error.span); +export function getDeclarationDiagnostics( + declarations: ng.Declaration[], modules: NgAnalyzedModules): ng.Diagnostic[] { + const directives = new Set(); + for (const ngModule of modules.ngModules) { + for (const directive of ngModule.declaredDirectives) { + directives.add(directive.reference); } - if (declaration.metadata) { - if (declaration.metadata.isComponent) { - if (!modules.ngModuleByPipeOrDirective.has(declaration.type)) { - report( - `Component '${declaration.type.name}' is not included in a module and will not be available inside a template. Consider adding it to a NgModule declaration`); - } - const {template, templateUrl} = declaration.metadata.template !; - if (template === null && !templateUrl) { - report(`Component '${declaration.type.name}' must have a template or templateUrl`); - } else if (template && templateUrl) { - report( - `Component '${declaration.type.name}' must not have both template and templateUrl`); - } - } else { - if (!directives) { - directives = new Set(); - modules.ngModules.forEach(module => { - module.declaredDirectives.forEach( - directive => { directives !.add(directive.reference); }); - }); - } - if (!directives.has(declaration.type)) { - report( - `Directive '${declaration.type.name}' is not included in a module and will not be available inside a template. Consider adding it to a NgModule declaration`); - } + } + + const results: ng.Diagnostic[] = []; + + for (const declaration of declarations) { + const {errors, metadata, type, declarationSpan} = declaration; + for (const error of errors) { + results.push({ + kind: ng.DiagnosticKind.Error, + message: error.message, + span: error.span, + }); + } + if (!metadata) { + continue; // declaration is not an Angular directive + } + if (metadata.isComponent) { + if (!modules.ngModuleByPipeOrDirective.has(declaration.type)) { + results.push({ + kind: ng.DiagnosticKind.Error, + message: missingDirective(type.name, metadata.isComponent), + span: declarationSpan, + }); } + const {template, templateUrl} = metadata.template !; + if (template === null && !templateUrl) { + results.push({ + kind: ng.DiagnosticKind.Error, + message: `Component '${type.name}' must have a template or templateUrl`, + span: declarationSpan, + }); + } else if (template && templateUrl) { + results.push({ + kind: ng.DiagnosticKind.Error, + message: `Component '${type.name}' must not have both template and templateUrl`, + span: declarationSpan, + }); + } + } else if (!directives.has(declaration.type)) { + results.push({ + kind: ng.DiagnosticKind.Error, + message: missingDirective(type.name, metadata.isComponent), + span: declarationSpan, + }); } } return results; } -function diagnosticChainToDiagnosticChain(chain: DiagnosticMessageChain): - ts.DiagnosticMessageChain { +/** + * Return a recursive data structure that chains diagnostic messages. + * @param chain + */ +function chainDiagnostics(chain: ng.DiagnosticMessageChain): ts.DiagnosticMessageChain { return { messageText: chain.message, category: ts.DiagnosticCategory.Error, code: 0, - next: chain.next ? diagnosticChainToDiagnosticChain(chain.next) : undefined + next: chain.next ? chainDiagnostics(chain.next) : undefined }; } -function diagnosticMessageToDiagnosticMessageText(message: string | DiagnosticMessageChain): string| - ts.DiagnosticMessageChain { - if (typeof message === 'string') { - return message; - } - return diagnosticChainToDiagnosticChain(message); -} - +/** + * Convert ng.Diagnostic to ts.Diagnostic. + * @param d diagnostic + * @param file + */ export function ngDiagnosticToTsDiagnostic( - d: Diagnostic, file: ts.SourceFile | undefined): ts.Diagnostic { + d: ng.Diagnostic, file: ts.SourceFile | undefined): ts.Diagnostic { return { file, start: d.span.start, length: d.span.end - d.span.start, - messageText: diagnosticMessageToDiagnosticMessageText(d.message), + messageText: typeof d.message === 'string' ? d.message : chainDiagnostics(d.message), category: ts.DiagnosticCategory.Error, code: 0, source: 'ng', }; } -export function uniqueBySpan(elements: T[]): T[] { +/** + * Return elements filtered by unique span. + * @param elements + */ +export function uniqueBySpan(elements: T[]): T[] { const result: T[] = []; const map = new Map>(); for (const element of elements) {