TypeScript compiler will now build to ES2015 code and modules. Babili is used to minify ES2015 code, providing an initial optimization that we couldn't previously get just from Uglify. Uses Babel to convert ES2015 to UMD/ES5 code, and Uglify to minimize the output.
250 lines
8.5 KiB
TypeScript
250 lines
8.5 KiB
TypeScript
/**
|
|
* @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 {AST, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, CompileDirectiveMetadata, CompileDirectiveSummary, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgAnalyzedModules, NgContentAst, ReferenceAst, StaticSymbol, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler';
|
|
|
|
import {AstResult, SelectorInfo, TemplateInfo} from './common';
|
|
import {getExpressionDiagnostics, getExpressionScope} from './expressions';
|
|
import {HtmlAstPath} from './html_path';
|
|
import {NullTemplateVisitor, TemplateAstChildVisitor, TemplateAstPath} from './template_path';
|
|
import {Declaration, Declarations, Diagnostic, DiagnosticKind, Diagnostics, Span, SymbolTable, TemplateSource} from './types';
|
|
import {getSelectors, hasTemplateReference, 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<Diagnostic>(
|
|
e => ({
|
|
kind: DiagnosticKind.Error,
|
|
span: offsetSpan(spanOf(e.span), template.span.start),
|
|
message: e.msg
|
|
})));
|
|
} else if (ast.templateAst) {
|
|
const expressionDiagnostics = getTemplateExpressionDiagnostics(template, ast);
|
|
results.push(...expressionDiagnostics);
|
|
}
|
|
if (ast.errors) {
|
|
results.push(...ast.errors.map<Diagnostic>(
|
|
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 = [];
|
|
|
|
let directives: Set<StaticSymbol>|undefined = undefined;
|
|
for (const declaration of declarations) {
|
|
const report = (message: string, span?: Span) => {
|
|
results.push(<Diagnostic>{
|
|
kind: DiagnosticKind.Error,
|
|
span: span || declaration.declarationSpan, message
|
|
});
|
|
};
|
|
for (const error of declaration.errors) {
|
|
report(error.message, error.span);
|
|
}
|
|
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`);
|
|
}
|
|
if (declaration.metadata.template.template == null &&
|
|
!declaration.metadata.template.templateUrl) {
|
|
report(`Component ${declaration.type.name} must have a template or 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`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
function getTemplateExpressionDiagnostics(
|
|
template: TemplateSource, astResult: AstResult): Diagnostics {
|
|
const info: TemplateInfo = {
|
|
template,
|
|
htmlAst: astResult.htmlAst,
|
|
directive: astResult.directive,
|
|
directives: astResult.directives,
|
|
pipes: astResult.pipes,
|
|
templateAst: astResult.templateAst,
|
|
expressionParser: astResult.expressionParser
|
|
};
|
|
const visitor = new ExpressionDiagnosticsVisitor(
|
|
info, (path: TemplateAstPath, includeEvent: boolean) =>
|
|
getExpressionScope(info, path, includeEvent));
|
|
templateVisitAll(visitor, astResult.templateAst);
|
|
return visitor.diagnostics;
|
|
}
|
|
|
|
class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor {
|
|
private path: TemplateAstPath;
|
|
private directiveSummary: CompileDirectiveSummary;
|
|
|
|
diagnostics: Diagnostics = [];
|
|
|
|
constructor(
|
|
private info: TemplateInfo,
|
|
private getExpressionScope: (path: TemplateAstPath, includeEvent: boolean) => SymbolTable) {
|
|
super();
|
|
this.path = new TemplateAstPath([], 0);
|
|
}
|
|
|
|
visitDirective(ast: DirectiveAst, context: any): any {
|
|
// Override the default child visitor to ignore the host properties of a directive.
|
|
if (ast.inputs && ast.inputs.length) {
|
|
templateVisitAll(this, ast.inputs, context);
|
|
}
|
|
}
|
|
|
|
visitBoundText(ast: BoundTextAst): void {
|
|
this.push(ast);
|
|
this.diagnoseExpression(ast.value, ast.sourceSpan.start.offset, false);
|
|
this.pop();
|
|
}
|
|
|
|
visitDirectiveProperty(ast: BoundDirectivePropertyAst): void {
|
|
this.push(ast);
|
|
this.diagnoseExpression(ast.value, this.attributeValueLocation(ast), false);
|
|
this.pop();
|
|
}
|
|
|
|
visitElementProperty(ast: BoundElementPropertyAst): void {
|
|
this.push(ast);
|
|
this.diagnoseExpression(ast.value, this.attributeValueLocation(ast), false);
|
|
this.pop();
|
|
}
|
|
|
|
visitEvent(ast: BoundEventAst): void {
|
|
this.push(ast);
|
|
this.diagnoseExpression(ast.handler, this.attributeValueLocation(ast), true);
|
|
this.pop();
|
|
}
|
|
|
|
visitVariable(ast: VariableAst): void {
|
|
const directive = this.directiveSummary;
|
|
if (directive && ast.value) {
|
|
const context = this.info.template.query.getTemplateContext(directive.type.reference);
|
|
if (!context.has(ast.value)) {
|
|
if (ast.value === '$implicit') {
|
|
this.reportError(
|
|
'The template context does not have an implicit value', spanOf(ast.sourceSpan));
|
|
} else {
|
|
this.reportError(
|
|
`The template context does not defined a member called '${ast.value}'`,
|
|
spanOf(ast.sourceSpan));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
visitElement(ast: ElementAst, context: any): void {
|
|
this.push(ast);
|
|
super.visitElement(ast, context);
|
|
this.pop();
|
|
}
|
|
|
|
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
|
|
const previousDirectiveSummary = this.directiveSummary;
|
|
|
|
this.push(ast);
|
|
|
|
// Find directive that refernces this template
|
|
this.directiveSummary =
|
|
ast.directives.map(d => d.directive).find(d => hasTemplateReference(d.type));
|
|
|
|
// Process children
|
|
super.visitEmbeddedTemplate(ast, context);
|
|
|
|
this.pop();
|
|
|
|
this.directiveSummary = previousDirectiveSummary;
|
|
}
|
|
|
|
private attributeValueLocation(ast: TemplateAst) {
|
|
const path = new HtmlAstPath(this.info.htmlAst, ast.sourceSpan.start.offset);
|
|
const last = path.tail;
|
|
if (last instanceof Attribute && last.valueSpan) {
|
|
// Add 1 for the quote.
|
|
return last.valueSpan.start.offset + 1;
|
|
}
|
|
return ast.sourceSpan.start.offset;
|
|
}
|
|
|
|
private diagnoseExpression(ast: AST, offset: number, includeEvent: boolean) {
|
|
const scope = this.getExpressionScope(this.path, includeEvent);
|
|
this.diagnostics.push(
|
|
...getExpressionDiagnostics(scope, ast, this.info.template.query, {
|
|
event: includeEvent
|
|
}).map(d => ({
|
|
span: offsetSpan(d.ast.span, offset + this.info.template.span.start),
|
|
kind: d.kind,
|
|
message: d.message
|
|
})));
|
|
}
|
|
|
|
private push(ast: TemplateAst) { this.path.push(ast); }
|
|
|
|
private pop() { this.path.pop(); }
|
|
|
|
private _selectors: SelectorInfo;
|
|
private selectors(): SelectorInfo {
|
|
let result = this._selectors;
|
|
if (!result) {
|
|
this._selectors = result = getSelectors(this.info);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private findElement(position: number): Element {
|
|
const htmlPath = new HtmlAstPath(this.info.htmlAst, position);
|
|
if (htmlPath.tail instanceof Element) {
|
|
return htmlPath.tail;
|
|
}
|
|
}
|
|
|
|
private reportError(message: string, span: Span) {
|
|
this.diagnostics.push({
|
|
span: offsetSpan(span, this.info.template.span.start),
|
|
kind: DiagnosticKind.Error, message
|
|
});
|
|
}
|
|
|
|
private reportWarning(message: string, span: Span) {
|
|
this.diagnostics.push({
|
|
span: offsetSpan(span, this.info.template.span.start),
|
|
kind: DiagnosticKind.Warning, message
|
|
});
|
|
}
|
|
}
|