refactor(ivy): use absolute source spans in type checker (#34417)

Previously, the type checker would compute an absolute source span by
combining an expression AST node's `ParseSpan` (relative to the start of
the expression) together with the absolute offset of the expression as
represented in a `ParseSourceSpan`, to arrive at a span relative to the
start of the file. This information is now directly available on an
expression AST node in the `AST.sourceSpan` property, which can be used
instead.

PR Close #34417
This commit is contained in:
JoostK 2019-12-15 14:28:51 +01:00 committed by Kara Erickson
parent 23595272fe
commit 8c6468a025
7 changed files with 75 additions and 121 deletions

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {AbsoluteSourceSpan, ParseSourceSpan, ParseSpan, Position} from '@angular/compiler'; import {AbsoluteSourceSpan, ParseSourceSpan} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {getTokenAtPosition} from '../../util/src/typescript'; import {getTokenAtPosition} from '../../util/src/typescript';
@ -35,26 +35,6 @@ export interface TcbSourceResolver {
sourceLocationToSpan(location: SourceLocation): ParseSourceSpan|null; sourceLocationToSpan(location: SourceLocation): ParseSourceSpan|null;
} }
/**
* An `AbsoluteSpan` is the result of translating the `ParseSpan` of `AST` template expression nodes
* to their absolute positions, as the `ParseSpan` is always relative to the start of the
* expression, not the full template.
*/
export interface AbsoluteSpan {
__brand__: 'AbsoluteSpan';
start: number;
end: number;
}
/**
* Translates a `ParseSpan` into an `AbsoluteSpan` by incorporating the location information that
* the `ParseSourceSpan` represents.
*/
export function toAbsoluteSpan(span: ParseSpan, sourceSpan: ParseSourceSpan): AbsoluteSpan {
const offset = sourceSpan.start.offset;
return <AbsoluteSpan>{start: span.start + offset, end: span.end + offset};
}
export function absoluteSourceSpanToSourceLocation( export function absoluteSourceSpanToSourceLocation(
id: string, span: AbsoluteSourceSpan): SourceLocation { id: string, span: AbsoluteSourceSpan): SourceLocation {
return {id, ...span}; return {id, ...span};
@ -78,20 +58,15 @@ export function wrapForDiagnostics(expr: ts.Expression): ts.Expression {
* Adds a synthetic comment to the expression that represents the parse span of the provided node. * Adds a synthetic comment to the expression that represents the parse span of the provided node.
* This comment can later be retrieved as trivia of a node to recover original source locations. * This comment can later be retrieved as trivia of a node to recover original source locations.
*/ */
export function addParseSpanInfo(node: ts.Node, span: AbsoluteSpan | ParseSourceSpan): void { export function addParseSpanInfo(node: ts.Node, span: AbsoluteSourceSpan | ParseSourceSpan): void {
let commentText: string; let commentText: string;
if (isAbsoluteSpan(span)) { if (span instanceof AbsoluteSourceSpan) {
commentText = `${span.start},${span.end}`; commentText = `${span.start},${span.end}`;
} else { } else {
commentText = `${span.start.offset},${span.end.offset}`; commentText = `${span.start.offset},${span.end.offset}`;
} }
ts.addSyntheticTrailingComment( ts.addSyntheticTrailingComment(
node, ts.SyntaxKind.MultiLineCommentTrivia, commentText, node, ts.SyntaxKind.MultiLineCommentTrivia, commentText, /* hasTrailingNewLine */ false);
/* hasTrailingNewLine */ false);
}
function isAbsoluteSpan(span: AbsoluteSpan | ParseSourceSpan): span is AbsoluteSpan {
return typeof span.start === 'number';
} }
/** /**
@ -245,18 +220,18 @@ export function makeTemplateDiagnostic(
function findSourceLocation(node: ts.Node, sourceFile: ts.SourceFile): SourceLocation|null { function findSourceLocation(node: ts.Node, sourceFile: ts.SourceFile): SourceLocation|null {
// Search for comments until the TCB's function declaration is encountered. // Search for comments until the TCB's function declaration is encountered.
while (node !== undefined && !ts.isFunctionDeclaration(node)) { while (node !== undefined && !ts.isFunctionDeclaration(node)) {
const parseSpan = const sourceSpan =
ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => { ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => {
if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) { if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) {
return null; return null;
} }
const commentText = sourceFile.text.substring(pos, end); const commentText = sourceFile.text.substring(pos, end);
return parseParseSpanComment(commentText); return parseSourceSpanComment(commentText);
}) || null; }) || null;
if (parseSpan !== null) { if (sourceSpan !== null) {
// Once the positional information has been extracted, search further up the TCB to extract // Once the positional information has been extracted, search further up the TCB to extract
// the file information that is attached with the TCB's function declaration. // the file information that is attached with the TCB's function declaration.
return toSourceLocation(parseSpan, node, sourceFile); return toSourceLocation(sourceSpan, node, sourceFile);
} }
node = node.parent; node = node.parent;
@ -266,7 +241,7 @@ function findSourceLocation(node: ts.Node, sourceFile: ts.SourceFile): SourceLoc
} }
function toSourceLocation( function toSourceLocation(
parseSpan: ParseSpan, node: ts.Node, sourceFile: ts.SourceFile): SourceLocation|null { sourceSpan: AbsoluteSourceSpan, node: ts.Node, sourceFile: ts.SourceFile): SourceLocation|null {
// Walk up to the function declaration of the TCB, the file information is attached there. // Walk up to the function declaration of the TCB, the file information is attached there.
let tcb = node; let tcb = node;
while (!ts.isFunctionDeclaration(tcb)) { while (!ts.isFunctionDeclaration(tcb)) {
@ -292,18 +267,18 @@ function toSourceLocation(
return { return {
id, id,
start: parseSpan.start, start: sourceSpan.start,
end: parseSpan.end, end: sourceSpan.end,
}; };
} }
const parseSpanComment = /^\/\*(\d+),(\d+)\*\/$/; const parseSpanComment = /^\/\*(\d+),(\d+)\*\/$/;
function parseParseSpanComment(commentText: string): ParseSpan|null { function parseSourceSpanComment(commentText: string): AbsoluteSourceSpan|null {
const match = commentText.match(parseSpanComment); const match = commentText.match(parseSpanComment);
if (match === null) { if (match === null) {
return null; return null;
} }
return new ParseSpan(+match[1], +match[2]); return new AbsoluteSourceSpan(+match[1], +match[2]);
} }

View File

@ -6,11 +6,11 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler'; import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {TypeCheckingConfig} from './api'; import {TypeCheckingConfig} from './api';
import {AbsoluteSpan, addParseSpanInfo, wrapForDiagnostics} from './diagnostics'; import {addParseSpanInfo, wrapForDiagnostics} from './diagnostics';
export const NULL_AS_ANY = export const NULL_AS_ANY =
ts.createAsExpression(ts.createNull(), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); ts.createAsExpression(ts.createNull(), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
@ -41,17 +41,16 @@ const BINARY_OPS = new Map<string, ts.SyntaxKind>([
* AST. * AST.
*/ */
export function astToTypescript( export function astToTypescript(
ast: AST, maybeResolve: (ast: AST) => (ts.Expression | null), config: TypeCheckingConfig, ast: AST, maybeResolve: (ast: AST) => (ts.Expression | null),
translateSpan: (span: ParseSpan) => AbsoluteSpan): ts.Expression { config: TypeCheckingConfig): ts.Expression {
const translator = new AstTranslator(maybeResolve, config, translateSpan); const translator = new AstTranslator(maybeResolve, config);
return translator.translate(ast); return translator.translate(ast);
} }
class AstTranslator implements AstVisitor { class AstTranslator implements AstVisitor {
constructor( constructor(
private maybeResolve: (ast: AST) => (ts.Expression | null), private maybeResolve: (ast: AST) => (ts.Expression | null),
private config: TypeCheckingConfig, private config: TypeCheckingConfig) {}
private translateSpan: (span: ParseSpan) => AbsoluteSpan) {}
translate(ast: AST): ts.Expression { translate(ast: AST): ts.Expression {
// Skip over an `ASTWithSource` as its `visit` method calls directly into its ast's `visit`, // Skip over an `ASTWithSource` as its `visit` method calls directly into its ast's `visit`,
@ -82,14 +81,14 @@ class AstTranslator implements AstVisitor {
throw new Error(`Unsupported Binary.operation: ${ast.operation}`); throw new Error(`Unsupported Binary.operation: ${ast.operation}`);
} }
const node = ts.createBinary(lhs, op as any, rhs); const node = ts.createBinary(lhs, op as any, rhs);
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
visitChain(ast: Chain): ts.Expression { visitChain(ast: Chain): ts.Expression {
const elements = ast.expressions.map(expr => this.translate(expr)); const elements = ast.expressions.map(expr => this.translate(expr));
const node = wrapForDiagnostics(ts.createCommaList(elements)); const node = wrapForDiagnostics(ts.createCommaList(elements));
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
@ -98,7 +97,7 @@ class AstTranslator implements AstVisitor {
const trueExpr = this.translate(ast.trueExp); const trueExpr = this.translate(ast.trueExp);
const falseExpr = this.translate(ast.falseExp); const falseExpr = this.translate(ast.falseExp);
const node = ts.createParen(ts.createConditional(condExpr, trueExpr, falseExpr)); const node = ts.createParen(ts.createConditional(condExpr, trueExpr, falseExpr));
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
@ -106,7 +105,7 @@ class AstTranslator implements AstVisitor {
const receiver = wrapForDiagnostics(this.translate(ast.target !)); const receiver = wrapForDiagnostics(this.translate(ast.target !));
const args = ast.args.map(expr => this.translate(expr)); const args = ast.args.map(expr => this.translate(expr));
const node = ts.createCall(receiver, undefined, args); const node = ts.createCall(receiver, undefined, args);
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
@ -127,7 +126,7 @@ class AstTranslator implements AstVisitor {
const receiver = wrapForDiagnostics(this.translate(ast.obj)); const receiver = wrapForDiagnostics(this.translate(ast.obj));
const key = this.translate(ast.key); const key = this.translate(ast.key);
const node = ts.createElementAccess(receiver, key); const node = ts.createElementAccess(receiver, key);
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
@ -138,14 +137,14 @@ class AstTranslator implements AstVisitor {
// available on `ast`. // available on `ast`.
const right = this.translate(ast.value); const right = this.translate(ast.value);
const node = wrapForDiagnostics(ts.createBinary(left, ts.SyntaxKind.EqualsToken, right)); const node = wrapForDiagnostics(ts.createBinary(left, ts.SyntaxKind.EqualsToken, right));
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
visitLiteralArray(ast: LiteralArray): ts.Expression { visitLiteralArray(ast: LiteralArray): ts.Expression {
const elements = ast.expressions.map(expr => this.translate(expr)); const elements = ast.expressions.map(expr => this.translate(expr));
const node = ts.createArrayLiteral(elements); const node = ts.createArrayLiteral(elements);
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
@ -155,7 +154,7 @@ class AstTranslator implements AstVisitor {
return ts.createPropertyAssignment(ts.createStringLiteral(key), value); return ts.createPropertyAssignment(ts.createStringLiteral(key), value);
}); });
const node = ts.createObjectLiteral(properties, true); const node = ts.createObjectLiteral(properties, true);
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
@ -168,7 +167,7 @@ class AstTranslator implements AstVisitor {
} else { } else {
node = ts.createLiteral(ast.value); node = ts.createLiteral(ast.value);
} }
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
@ -177,14 +176,14 @@ class AstTranslator implements AstVisitor {
const method = ts.createPropertyAccess(receiver, ast.name); const method = ts.createPropertyAccess(receiver, ast.name);
const args = ast.args.map(expr => this.translate(expr)); const args = ast.args.map(expr => this.translate(expr));
const node = ts.createCall(method, undefined, args); const node = ts.createCall(method, undefined, args);
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
visitNonNullAssert(ast: NonNullAssert): ts.Expression { visitNonNullAssert(ast: NonNullAssert): ts.Expression {
const expr = wrapForDiagnostics(this.translate(ast.expression)); const expr = wrapForDiagnostics(this.translate(ast.expression));
const node = ts.createNonNullExpression(expr); const node = ts.createNonNullExpression(expr);
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
@ -193,7 +192,7 @@ class AstTranslator implements AstVisitor {
visitPrefixNot(ast: PrefixNot): ts.Expression { visitPrefixNot(ast: PrefixNot): ts.Expression {
const expression = wrapForDiagnostics(this.translate(ast.expression)); const expression = wrapForDiagnostics(this.translate(ast.expression));
const node = ts.createLogicalNot(expression); const node = ts.createLogicalNot(expression);
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
@ -202,7 +201,7 @@ class AstTranslator implements AstVisitor {
// TypeScript expression to read the property. // TypeScript expression to read the property.
const receiver = wrapForDiagnostics(this.translate(ast.receiver)); const receiver = wrapForDiagnostics(this.translate(ast.receiver));
const node = ts.createPropertyAccess(receiver, ast.name); const node = ts.createPropertyAccess(receiver, ast.name);
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
@ -213,7 +212,7 @@ class AstTranslator implements AstVisitor {
// available on `ast`. // available on `ast`.
const right = this.translate(ast.value); const right = this.translate(ast.value);
const node = wrapForDiagnostics(ts.createBinary(left, ts.SyntaxKind.EqualsToken, right)); const node = wrapForDiagnostics(ts.createBinary(left, ts.SyntaxKind.EqualsToken, right));
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
@ -228,7 +227,7 @@ class AstTranslator implements AstVisitor {
const expr = ts.createCall(method, undefined, args); const expr = ts.createCall(method, undefined, args);
const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY; const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY;
const node = safeTernary(receiver, expr, whenNull); const node = safeTernary(receiver, expr, whenNull);
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
@ -241,7 +240,7 @@ class AstTranslator implements AstVisitor {
const expr = ts.createPropertyAccess(ts.createNonNullExpression(receiver), ast.name); const expr = ts.createPropertyAccess(ts.createNonNullExpression(receiver), ast.name);
const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY; const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY;
const node = safeTernary(receiver, expr, whenNull); const node = safeTernary(receiver, expr, whenNull);
addParseSpanInfo(node, this.translateSpan(ast.span)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} }
} }

View File

@ -44,15 +44,11 @@ export interface OutOfBandDiagnosticRecorder {
* @param templateId the template type-checking ID of the template which contains the unknown * @param templateId the template type-checking ID of the template which contains the unknown
* pipe. * pipe.
* @param ast the `BindingPipe` invocation of the pipe which could not be found. * @param ast the `BindingPipe` invocation of the pipe which could not be found.
* @param sourceSpan the `AbsoluteSourceSpan` of the pipe invocation (ideally, this should be the
* source span of the pipe's name). This depends on the source span of the `BindingPipe` itself
* plus span of the larger expression context.
*/ */
missingPipe(templateId: string, ast: BindingPipe, sourceSpan: AbsoluteSourceSpan): void; missingPipe(templateId: string, ast: BindingPipe): void;
illegalAssignmentToTemplateVar( illegalAssignmentToTemplateVar(
templateId: string, assignment: PropertyWrite, assignmentSpan: AbsoluteSourceSpan, templateId: string, assignment: PropertyWrite, target: TmplAstVariable): void;
target: TmplAstVariable): void;
} }
export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecorder { export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecorder {
@ -72,11 +68,11 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor
ngErrorCode(ErrorCode.MISSING_REFERENCE_TARGET), errorMsg)); ngErrorCode(ErrorCode.MISSING_REFERENCE_TARGET), errorMsg));
} }
missingPipe(templateId: string, ast: BindingPipe, absSpan: AbsoluteSourceSpan): void { missingPipe(templateId: string, ast: BindingPipe): void {
const mapping = this.resolver.getSourceMapping(templateId); const mapping = this.resolver.getSourceMapping(templateId);
const errorMsg = `No pipe found with name '${ast.name}'.`; const errorMsg = `No pipe found with name '${ast.name}'.`;
const location = absoluteSourceSpanToSourceLocation(templateId, absSpan); const location = absoluteSourceSpanToSourceLocation(templateId, ast.nameSpan);
const sourceSpan = this.resolver.sourceLocationToSpan(location); const sourceSpan = this.resolver.sourceLocationToSpan(location);
if (sourceSpan === null) { if (sourceSpan === null) {
throw new Error( throw new Error(
@ -88,13 +84,12 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor
} }
illegalAssignmentToTemplateVar( illegalAssignmentToTemplateVar(
templateId: string, assignment: PropertyWrite, assignmentSpan: AbsoluteSourceSpan, templateId: string, assignment: PropertyWrite, target: TmplAstVariable): void {
target: TmplAstVariable): void {
const mapping = this.resolver.getSourceMapping(templateId); const mapping = this.resolver.getSourceMapping(templateId);
const errorMsg = const errorMsg =
`Cannot use variable '${assignment.name}' as the left-hand side of an assignment expression. Template variables are read-only.`; `Cannot use variable '${assignment.name}' as the left-hand side of an assignment expression. Template variables are read-only.`;
const location = absoluteSourceSpanToSourceLocation(templateId, assignmentSpan); const location = absoluteSourceSpanToSourceLocation(templateId, assignment.sourceSpan);
const sourceSpan = this.resolver.sourceLocationToSpan(location); const sourceSpan = this.resolver.sourceLocationToSpan(location);
if (sourceSpan === null) { if (sourceSpan === null) {
throw new Error(`Assertion failure: no SourceLocation found for property binding.`); throw new Error(`Assertion failure: no SourceLocation found for property binding.`);

View File

@ -6,9 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AST, BoundTarget, ImplicitReceiver, ParseSourceSpan, PropertyWrite, RecursiveAstVisitor, TmplAstVariable} from '@angular/compiler'; import {AST, BoundTarget, ImplicitReceiver, PropertyWrite, RecursiveAstVisitor, TmplAstVariable} from '@angular/compiler';
import {toAbsoluteSpan} from './diagnostics';
import {OutOfBandDiagnosticRecorder} from './oob'; import {OutOfBandDiagnosticRecorder} from './oob';
/** /**
@ -17,7 +16,7 @@ import {OutOfBandDiagnosticRecorder} from './oob';
export class ExpressionSemanticVisitor extends RecursiveAstVisitor { export class ExpressionSemanticVisitor extends RecursiveAstVisitor {
constructor( constructor(
private templateId: string, private boundTarget: BoundTarget<any>, private templateId: string, private boundTarget: BoundTarget<any>,
private oob: OutOfBandDiagnosticRecorder, private sourceSpan: ParseSourceSpan) { private oob: OutOfBandDiagnosticRecorder) {
super(); super();
} }
@ -31,14 +30,12 @@ export class ExpressionSemanticVisitor extends RecursiveAstVisitor {
const target = this.boundTarget.getExpressionTarget(ast); const target = this.boundTarget.getExpressionTarget(ast);
if (target instanceof TmplAstVariable) { if (target instanceof TmplAstVariable) {
// Template variables are read-only. // Template variables are read-only.
const astSpan = toAbsoluteSpan(ast.span, this.sourceSpan); this.oob.illegalAssignmentToTemplateVar(this.templateId, ast, target);
this.oob.illegalAssignmentToTemplateVar(this.templateId, ast, astSpan, target);
} }
} }
static visit( static visit(
ast: AST, sourceSpan: ParseSourceSpan, id: string, boundTarget: BoundTarget<any>, ast: AST, id: string, boundTarget: BoundTarget<any>, oob: OutOfBandDiagnosticRecorder): void {
oob: OutOfBandDiagnosticRecorder): void { ast.visit(new ExpressionSemanticVisitor(id, boundTarget, oob));
ast.visit(new ExpressionSemanticVisitor(id, boundTarget, oob, sourceSpan));
} }
} }

View File

@ -6,14 +6,14 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AST, BindingPipe, BindingType, BoundTarget, DYNAMIC_TYPE, ImplicitReceiver, MethodCall, ParseSourceSpan, ParseSpan, ParsedEventType, PropertyRead, PropertyWrite, SchemaMetadata, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler'; import {AST, BindingPipe, BindingType, BoundTarget, DYNAMIC_TYPE, ImplicitReceiver, MethodCall, ParseSourceSpan, ParsedEventType, PropertyRead, PropertyWrite, SchemaMetadata, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Reference} from '../../imports'; import {Reference} from '../../imports';
import {ClassDeclaration} from '../../reflection'; import {ClassDeclaration} from '../../reflection';
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta} from './api'; import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta} from './api';
import {addParseSpanInfo, addSourceId, toAbsoluteSpan, wrapForDiagnostics} from './diagnostics'; import {addParseSpanInfo, addSourceId, wrapForDiagnostics} from './diagnostics';
import {DomSchemaChecker} from './dom'; import {DomSchemaChecker} from './dom';
import {Environment} from './environment'; import {Environment} from './environment';
import {NULL_AS_ANY, astToTypescript} from './expression'; import {NULL_AS_ANY, astToTypescript} from './expression';
@ -215,9 +215,7 @@ class TcbTemplateBodyOp extends TcbOp {
i instanceof TmplAstBoundAttribute && i.name === guard.inputName); i instanceof TmplAstBoundAttribute && i.name === guard.inputName);
if (boundInput !== undefined) { if (boundInput !== undefined) {
// If there is such a binding, generate an expression for it. // If there is such a binding, generate an expression for it.
const expr = tcbExpression( const expr = tcbExpression(boundInput.value, this.tcb, this.scope);
boundInput.value, this.tcb, this.scope,
boundInput.valueSpan || boundInput.sourceSpan);
if (guard.type === 'binding') { if (guard.type === 'binding') {
// Use the binding expression itself as guard. // Use the binding expression itself as guard.
@ -229,8 +227,7 @@ class TcbTemplateBodyOp extends TcbOp {
dirInstId, dirInstId,
expr, expr,
]); ]);
addParseSpanInfo( addParseSpanInfo(guardInvoke, boundInput.value.sourceSpan);
guardInvoke, toAbsoluteSpan(boundInput.value.span, boundInput.sourceSpan));
directiveGuards.push(guardInvoke); directiveGuards.push(guardInvoke);
} }
} }
@ -281,7 +278,7 @@ class TcbTextInterpolationOp extends TcbOp {
} }
execute(): null { execute(): null {
const expr = tcbExpression(this.binding.value, this.tcb, this.scope, this.binding.sourceSpan); const expr = tcbExpression(this.binding.value, this.tcb, this.scope);
this.scope.addStatement(ts.createExpressionStatement(expr)); this.scope.addStatement(ts.createExpressionStatement(expr));
return null; return null;
} }
@ -400,8 +397,7 @@ class TcbUnclaimedInputsOp extends TcbOp {
continue; continue;
} }
let expr = tcbExpression( let expr = tcbExpression(binding.value, this.tcb, this.scope);
binding.value, this.tcb, this.scope, binding.valueSpan || binding.sourceSpan);
if (!this.tcb.env.config.checkTypeOfInputBindings) { if (!this.tcb.env.config.checkTypeOfInputBindings) {
// If checking the type of bindings is disabled, cast the resulting expression to 'any' // If checking the type of bindings is disabled, cast the resulting expression to 'any'
// before the assignment. // before the assignment.
@ -494,8 +490,7 @@ class TcbDirectiveOutputsOp extends TcbOp {
} }
ExpressionSemanticVisitor.visit( ExpressionSemanticVisitor.visit(
output.handler, output.handlerSpan, this.tcb.id, this.tcb.boundTarget, output.handler, this.tcb.id, this.tcb.boundTarget, this.tcb.oobRecorder);
this.tcb.oobRecorder);
} }
return null; return null;
@ -556,8 +551,7 @@ class TcbUnclaimedOutputsOp extends TcbOp {
} }
ExpressionSemanticVisitor.visit( ExpressionSemanticVisitor.visit(
output.handler, output.handlerSpan, this.tcb.id, this.tcb.boundTarget, output.handler, this.tcb.id, this.tcb.boundTarget, this.tcb.oobRecorder);
this.tcb.oobRecorder);
} }
return null; return null;
@ -949,23 +943,19 @@ function tcbCtxParam(
* Process an `AST` expression and convert it into a `ts.Expression`, generating references to the * Process an `AST` expression and convert it into a `ts.Expression`, generating references to the
* correct identifiers in the current scope. * correct identifiers in the current scope.
*/ */
function tcbExpression( function tcbExpression(ast: AST, tcb: Context, scope: Scope): ts.Expression {
ast: AST, tcb: Context, scope: Scope, sourceSpan: ParseSourceSpan): ts.Expression { const translator = new TcbExpressionTranslator(tcb, scope);
const translator = new TcbExpressionTranslator(tcb, scope, sourceSpan);
return translator.translate(ast); return translator.translate(ast);
} }
class TcbExpressionTranslator { class TcbExpressionTranslator {
constructor( constructor(protected tcb: Context, protected scope: Scope) {}
protected tcb: Context, protected scope: Scope, protected sourceSpan: ParseSourceSpan) {}
translate(ast: AST): ts.Expression { translate(ast: AST): ts.Expression {
// `astToTypescript` actually does the conversion. A special resolver `tcbResolve` is passed // `astToTypescript` actually does the conversion. A special resolver `tcbResolve` is passed
// which interprets specific expression nodes that interact with the `ImplicitReceiver`. These // which interprets specific expression nodes that interact with the `ImplicitReceiver`. These
// nodes actually refer to identifiers within the current scope. // nodes actually refer to identifiers within the current scope.
return astToTypescript( return astToTypescript(ast, ast => this.resolve(ast), this.tcb.env.config);
ast, ast => this.resolve(ast), this.tcb.env.config,
(span: ParseSpan) => toAbsoluteSpan(span, this.sourceSpan));
} }
/** /**
@ -989,7 +979,7 @@ class TcbExpressionTranslator {
const expr = this.translate(ast.value); const expr = this.translate(ast.value);
const result = ts.createParen(ts.createBinary(target, ts.SyntaxKind.EqualsToken, expr)); const result = ts.createParen(ts.createBinary(target, ts.SyntaxKind.EqualsToken, expr));
addParseSpanInfo(result, toAbsoluteSpan(ast.span, this.sourceSpan)); addParseSpanInfo(result, ast.sourceSpan);
return result; return result;
} else if (ast instanceof ImplicitReceiver) { } else if (ast instanceof ImplicitReceiver) {
// AST instances representing variables and references look very similar to property reads // AST instances representing variables and references look very similar to property reads
@ -1012,8 +1002,7 @@ class TcbExpressionTranslator {
pipe = this.tcb.getPipeByName(ast.name); pipe = this.tcb.getPipeByName(ast.name);
if (pipe === null) { if (pipe === null) {
// No pipe by that name exists in scope. Record this as an error. // No pipe by that name exists in scope. Record this as an error.
const nameAbsoluteSpan = toAbsoluteSpan(ast.nameSpan, this.sourceSpan); this.tcb.oobRecorder.missingPipe(this.tcb.id, ast);
this.tcb.oobRecorder.missingPipe(this.tcb.id, ast, nameAbsoluteSpan);
// Return an 'any' value to at least allow the rest of the expression to be checked. // Return an 'any' value to at least allow the rest of the expression to be checked.
pipe = NULL_AS_ANY; pipe = NULL_AS_ANY;
@ -1024,7 +1013,7 @@ class TcbExpressionTranslator {
} }
const args = ast.args.map(arg => this.translate(arg)); const args = ast.args.map(arg => this.translate(arg));
const result = tsCallMethod(pipe, 'transform', [expr, ...args]); const result = tsCallMethod(pipe, 'transform', [expr, ...args]);
addParseSpanInfo(result, toAbsoluteSpan(ast.span, this.sourceSpan)); addParseSpanInfo(result, ast.sourceSpan);
return result; return result;
} else if (ast instanceof MethodCall && ast.receiver instanceof ImplicitReceiver) { } else if (ast instanceof MethodCall && ast.receiver instanceof ImplicitReceiver) {
// Resolve the special `$any(expr)` syntax to insert a cast of the argument to type `any`. // Resolve the special `$any(expr)` syntax to insert a cast of the argument to type `any`.
@ -1033,7 +1022,7 @@ class TcbExpressionTranslator {
const exprAsAny = const exprAsAny =
ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
const result = ts.createParen(exprAsAny); const result = ts.createParen(exprAsAny);
addParseSpanInfo(result, toAbsoluteSpan(ast.span, this.sourceSpan)); addParseSpanInfo(result, ast.sourceSpan);
return result; return result;
} }
@ -1049,7 +1038,7 @@ class TcbExpressionTranslator {
const method = ts.createPropertyAccess(wrapForDiagnostics(receiver), ast.name); const method = ts.createPropertyAccess(wrapForDiagnostics(receiver), ast.name);
const args = ast.args.map(arg => this.translate(arg)); const args = ast.args.map(arg => this.translate(arg));
const node = ts.createCall(method, undefined, args); const node = ts.createCall(method, undefined, args);
addParseSpanInfo(node, toAbsoluteSpan(ast.span, this.sourceSpan)); addParseSpanInfo(node, ast.sourceSpan);
return node; return node;
} else { } else {
// This AST isn't special after all. // This AST isn't special after all.
@ -1071,7 +1060,7 @@ class TcbExpressionTranslator {
// This expression has a binding to some variable or reference in the template. Resolve it. // This expression has a binding to some variable or reference in the template. Resolve it.
if (binding instanceof TmplAstVariable) { if (binding instanceof TmplAstVariable) {
const expr = ts.getMutableClone(this.scope.resolve(binding)); const expr = ts.getMutableClone(this.scope.resolve(binding));
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, this.sourceSpan)); addParseSpanInfo(expr, ast.sourceSpan);
return expr; return expr;
} else if (binding instanceof TmplAstReference) { } else if (binding instanceof TmplAstReference) {
const target = this.tcb.boundTarget.getReferenceTarget(binding); const target = this.tcb.boundTarget.getReferenceTarget(binding);
@ -1092,7 +1081,7 @@ class TcbExpressionTranslator {
} }
const expr = ts.getMutableClone(this.scope.resolve(target)); const expr = ts.getMutableClone(this.scope.resolve(target));
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, this.sourceSpan)); addParseSpanInfo(expr, ast.sourceSpan);
return expr; return expr;
} else if (target instanceof TmplAstTemplate) { } else if (target instanceof TmplAstTemplate) {
if (!this.tcb.env.config.checkTypeOfNonDomReferences) { if (!this.tcb.env.config.checkTypeOfNonDomReferences) {
@ -1109,7 +1098,7 @@ class TcbExpressionTranslator {
value, value,
this.tcb.env.referenceExternalType('@angular/core', 'TemplateRef', [DYNAMIC_TYPE])); this.tcb.env.referenceExternalType('@angular/core', 'TemplateRef', [DYNAMIC_TYPE]));
value = ts.createParen(value); value = ts.createParen(value);
addParseSpanInfo(value, toAbsoluteSpan(ast.span, this.sourceSpan)); addParseSpanInfo(value, ast.sourceSpan);
return value; return value;
} else { } else {
if (!this.tcb.env.config.checkTypeOfNonDomReferences) { if (!this.tcb.env.config.checkTypeOfNonDomReferences) {
@ -1118,7 +1107,7 @@ class TcbExpressionTranslator {
} }
const expr = ts.getMutableClone(this.scope.resolve(target.node, target.directive)); const expr = ts.getMutableClone(this.scope.resolve(target.node, target.directive));
addParseSpanInfo(expr, toAbsoluteSpan(ast.span, this.sourceSpan)); addParseSpanInfo(expr, ast.sourceSpan);
return expr; return expr;
} }
} else { } else {
@ -1236,7 +1225,7 @@ function tcbGetDirectiveInputs(
let expr: ts.Expression; let expr: ts.Expression;
if (attr instanceof TmplAstBoundAttribute) { if (attr instanceof TmplAstBoundAttribute) {
// Produce an expression representing the value of the binding. // Produce an expression representing the value of the binding.
expr = tcbExpression(attr.value, tcb, scope, attr.valueSpan || attr.sourceSpan); expr = tcbExpression(attr.value, tcb, scope);
} else { } else {
// For regular attributes with a static string value, use the represented string literal. // For regular attributes with a static string value, use the represented string literal.
expr = ts.createStringLiteral(attr.value); expr = ts.createStringLiteral(attr.value);
@ -1275,7 +1264,7 @@ const enum EventParamType {
function tcbCreateEventHandler( function tcbCreateEventHandler(
event: TmplAstBoundEvent, tcb: Context, scope: Scope, event: TmplAstBoundEvent, tcb: Context, scope: Scope,
eventType: EventParamType | ts.TypeNode): ts.ArrowFunction { eventType: EventParamType | ts.TypeNode): ts.ArrowFunction {
const handler = tcbEventHandlerExpression(event.handler, tcb, scope, event.handlerSpan); const handler = tcbEventHandlerExpression(event.handler, tcb, scope);
let eventParamType: ts.TypeNode|undefined; let eventParamType: ts.TypeNode|undefined;
if (eventType === EventParamType.Infer) { if (eventType === EventParamType.Infer) {
@ -1307,9 +1296,8 @@ function tcbCreateEventHandler(
* `ts.Expression`, with special handling of the `$event` variable that can be used within event * `ts.Expression`, with special handling of the `$event` variable that can be used within event
* bindings. * bindings.
*/ */
function tcbEventHandlerExpression( function tcbEventHandlerExpression(ast: AST, tcb: Context, scope: Scope): ts.Expression {
ast: AST, tcb: Context, scope: Scope, sourceSpan: ParseSourceSpan): ts.Expression { const translator = new TcbEventHandlerTranslator(tcb, scope);
const translator = new TcbEventHandlerTranslator(tcb, scope, sourceSpan);
return translator.translate(ast); return translator.translate(ast);
} }
@ -1322,7 +1310,7 @@ class TcbEventHandlerTranslator extends TcbExpressionTranslator {
if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver && if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver &&
ast.name === EVENT_PARAMETER) { ast.name === EVENT_PARAMETER) {
const event = ts.createIdentifier(EVENT_PARAMETER); const event = ts.createIdentifier(EVENT_PARAMETER);
addParseSpanInfo(event, toAbsoluteSpan(ast.span, this.sourceSpan)); addParseSpanInfo(event, ast.sourceSpan);
return event; return event;
} }

View File

@ -30,7 +30,7 @@ export class AST {
/** /**
* Absolute location of the expression AST in a source code file. * Absolute location of the expression AST in a source code file.
*/ */
public sourceSpan: Readonly<AbsoluteSourceSpan>) {} public sourceSpan: AbsoluteSourceSpan) {}
visit(visitor: AstVisitor, context: any = null): any { return null; } visit(visitor: AstVisitor, context: any = null): any { return null; }
toString(): string { return 'AST'; } toString(): string { return 'AST'; }
} }
@ -145,7 +145,7 @@ export class KeyedWrite extends AST {
export class BindingPipe extends AST { export class BindingPipe extends AST {
constructor( constructor(
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public exp: AST, public name: string, span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public exp: AST, public name: string,
public args: any[], public nameSpan: ParseSpan) { public args: any[], public nameSpan: AbsoluteSourceSpan) {
super(span, sourceSpan); super(span, sourceSpan);
} }
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitPipe(this, context); } visit(visitor: AstVisitor, context: any = null): any { return visitor.visitPipe(this, context); }

View File

@ -359,7 +359,7 @@ export class _ParseAST {
do { do {
const nameStart = this.inputIndex; const nameStart = this.inputIndex;
const name = this.expectIdentifierOrKeyword(); const name = this.expectIdentifierOrKeyword();
const nameSpan = this.span(nameStart); const nameSpan = this.sourceSpan(nameStart);
const args: AST[] = []; const args: AST[] = [];
while (this.optionalCharacter(chars.$COLON)) { while (this.optionalCharacter(chars.$COLON)) {
args.push(this.parseExpression()); args.push(this.parseExpression());