/** * @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 {NgAnalyzedModules} from '@angular/compiler'; import {getTemplateExpressionDiagnostics} from '@angular/compiler-cli/src/language_services'; import * as ts from 'typescript'; import {AstResult} from './common'; import * as ng from './types'; import {offsetSpan, spanOf} from './utils'; /** * Return diagnostic information for the parsed AST of the template. * @param ast contains HTML and template AST */ export function getTemplateDiagnostics(ast: AstResult): ng.Diagnostic[] { const results: ng.Diagnostic[] = []; const {parseErrors, templateAst, htmlAst, template} = ast; if (parseErrors) { results.push(...parseErrors.map(e => { return { kind: ng.DiagnosticKind.Error, span: offsetSpan(spanOf(e.span), template.span.start), message: e.msg, }; })); } const expressionDiagnostics = getTemplateExpressionDiagnostics({ templateAst: templateAst, htmlAst: htmlAst, offset: template.span.start, query: template.query, members: template.members, }); results.push(...expressionDiagnostics); return results; } /** * 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.'; } 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); } } 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; } /** * 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 ? chainDiagnostics(chain.next) : undefined }; } /** * Convert ng.Diagnostic to ts.Diagnostic. * @param d diagnostic * @param file */ export function ngDiagnosticToTsDiagnostic( d: ng.Diagnostic, file: ts.SourceFile | undefined): ts.Diagnostic { return { file, start: d.span.start, length: d.span.end - d.span.start, messageText: typeof d.message === 'string' ? d.message : chainDiagnostics(d.message), category: ts.DiagnosticCategory.Error, code: 0, source: 'ng', }; } /** * 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) { const {span} = element; let set = map.get(span.start); if (!set) { set = new Set(); map.set(span.start, set); } if (!set.has(span.end)) { set.add(span.end); result.push(element); } } return result; }