feat(ivy): include value spans for attributes, variables and references (#30181)

Template AST nodes for (bound) attributes, variables and references will
now retain a reference to the source span of their value, which allows
for more accurate type check diagnostics.

PR Close #30181
This commit is contained in:
JoostK 2019-05-04 22:41:17 +02:00 committed by Miško Hevery
parent 985513351b
commit 489cef6ea2
8 changed files with 395 additions and 61 deletions

View File

@ -198,7 +198,9 @@ 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(boundInput.value, this.tcb, this.scope, boundInput.sourceSpan); const expr = tcbExpression(
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.
@ -335,7 +337,8 @@ class TcbUnclaimedInputsOp extends TcbOp {
continue; continue;
} }
let expr = tcbExpression(binding.value, this.tcb, this.scope, binding.sourceSpan); let expr = tcbExpression(
binding.value, this.tcb, this.scope, binding.valueSpan || binding.sourceSpan);
// If checking the type of bindings is disabled, cast the resulting expression to 'any' before // If checking the type of bindings is disabled, cast the resulting expression to 'any' before
// the assignment. // the assignment.
@ -768,7 +771,7 @@ function tcbGetInputBindingExpressions(
function processAttribute(attr: TmplAstBoundAttribute | TmplAstTextAttribute): void { function processAttribute(attr: TmplAstBoundAttribute | TmplAstTextAttribute): void {
if (attr instanceof TmplAstBoundAttribute && propMatch.has(attr.name)) { if (attr instanceof TmplAstBoundAttribute && propMatch.has(attr.name)) {
// Produce an expression representing the value of the binding. // Produce an expression representing the value of the binding.
const expr = tcbExpression(attr.value, tcb, scope, attr.sourceSpan); const expr = tcbExpression(attr.value, tcb, scope, attr.valueSpan || attr.sourceSpan);
// Call the callback. // Call the callback.
bindings.push({ bindings.push({
property: attr.name, property: attr.name,

View File

@ -691,7 +691,7 @@ export class ParsedProperty {
constructor( constructor(
public name: string, public expression: ASTWithSource, public type: ParsedPropertyType, public name: string, public expression: ASTWithSource, public type: ParsedPropertyType,
public sourceSpan: ParseSourceSpan) { public sourceSpan: ParseSourceSpan, public valueSpan?: ParseSourceSpan) {
this.isLiteral = this.type === ParsedPropertyType.LITERAL_ATTR; this.isLiteral = this.type === ParsedPropertyType.LITERAL_ATTR;
this.isAnimation = this.type === ParsedPropertyType.ANIMATION; this.isAnimation = this.type === ParsedPropertyType.ANIMATION;
} }
@ -739,5 +739,6 @@ export const enum BindingType {
export class BoundElementProperty { export class BoundElementProperty {
constructor( constructor(
public name: string, public type: BindingType, public securityContext: SecurityContext, public name: string, public type: BindingType, public securityContext: SecurityContext,
public value: AST, public unit: string|null, public sourceSpan: ParseSourceSpan) {} public value: AST, public unit: string|null, public sourceSpan: ParseSourceSpan,
public valueSpan?: ParseSourceSpan) {}
} }

View File

@ -37,11 +37,12 @@ export class BoundAttribute implements Node {
constructor( constructor(
public name: string, public type: BindingType, public securityContext: SecurityContext, public name: string, public type: BindingType, public securityContext: SecurityContext,
public value: AST, public unit: string|null, public sourceSpan: ParseSourceSpan, public value: AST, public unit: string|null, public sourceSpan: ParseSourceSpan,
public i18n?: I18nAST) {} public valueSpan?: ParseSourceSpan, public i18n?: I18nAST) {}
static fromBoundElementProperty(prop: BoundElementProperty, i18n?: I18nAST) { static fromBoundElementProperty(prop: BoundElementProperty, i18n?: I18nAST) {
return new BoundAttribute( return new BoundAttribute(
prop.name, prop.type, prop.securityContext, prop.value, prop.unit, prop.sourceSpan, i18n); prop.name, prop.type, prop.securityContext, prop.value, prop.unit, prop.sourceSpan,
prop.valueSpan, i18n);
} }
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitBoundAttribute(this); } visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitBoundAttribute(this); }
@ -96,12 +97,16 @@ export class Content implements Node {
} }
export class Variable implements Node { export class Variable implements Node {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {} constructor(
public name: string, public value: string, public sourceSpan: ParseSourceSpan,
public valueSpan?: ParseSourceSpan) {}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitVariable(this); } visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitVariable(this); }
} }
export class Reference implements Node { export class Reference implements Node {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {} constructor(
public name: string, public value: string, public sourceSpan: ParseSourceSpan,
public valueSpan?: ParseSourceSpan) {}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitReference(this); } visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitReference(this); }
} }

View File

@ -297,20 +297,20 @@ class HtmlAstToIvyAst implements html.Visitor {
hasBinding = true; hasBinding = true;
if (bindParts[KW_BIND_IDX] != null) { if (bindParts[KW_BIND_IDX] != null) {
this.bindingParser.parsePropertyBinding( this.bindingParser.parsePropertyBinding(
bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, matchableAttributes, bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, attribute.valueSpan,
parsedProperties); matchableAttributes, parsedProperties);
} else if (bindParts[KW_LET_IDX]) { } else if (bindParts[KW_LET_IDX]) {
if (isTemplateElement) { if (isTemplateElement) {
const identifier = bindParts[IDENT_KW_IDX]; const identifier = bindParts[IDENT_KW_IDX];
this.parseVariable(identifier, value, srcSpan, variables); this.parseVariable(identifier, value, srcSpan, attribute.valueSpan, variables);
} else { } else {
this.reportError(`"let-" is only supported on ng-template elements.`, srcSpan); this.reportError(`"let-" is only supported on ng-template elements.`, srcSpan);
} }
} else if (bindParts[KW_REF_IDX]) { } else if (bindParts[KW_REF_IDX]) {
const identifier = bindParts[IDENT_KW_IDX]; const identifier = bindParts[IDENT_KW_IDX];
this.parseReference(identifier, value, srcSpan, references); this.parseReference(identifier, value, srcSpan, attribute.valueSpan, references);
} else if (bindParts[KW_ON_IDX]) { } else if (bindParts[KW_ON_IDX]) {
const events: ParsedEvent[] = []; const events: ParsedEvent[] = [];
@ -320,19 +320,20 @@ class HtmlAstToIvyAst implements html.Visitor {
addEvents(events, boundEvents); addEvents(events, boundEvents);
} else if (bindParts[KW_BINDON_IDX]) { } else if (bindParts[KW_BINDON_IDX]) {
this.bindingParser.parsePropertyBinding( this.bindingParser.parsePropertyBinding(
bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, matchableAttributes, bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, attribute.valueSpan,
parsedProperties); matchableAttributes, parsedProperties);
this.parseAssignmentEvent( this.parseAssignmentEvent(
bindParts[IDENT_KW_IDX], value, srcSpan, attribute.valueSpan, matchableAttributes, bindParts[IDENT_KW_IDX], value, srcSpan, attribute.valueSpan, matchableAttributes,
boundEvents); boundEvents);
} else if (bindParts[KW_AT_IDX]) { } else if (bindParts[KW_AT_IDX]) {
this.bindingParser.parseLiteralAttr( this.bindingParser.parseLiteralAttr(
name, value, srcSpan, absoluteOffset, matchableAttributes, parsedProperties); name, value, srcSpan, absoluteOffset, attribute.valueSpan, matchableAttributes,
parsedProperties);
} else if (bindParts[IDENT_BANANA_BOX_IDX]) { } else if (bindParts[IDENT_BANANA_BOX_IDX]) {
this.bindingParser.parsePropertyBinding( this.bindingParser.parsePropertyBinding(
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, absoluteOffset, bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, absoluteOffset,
matchableAttributes, parsedProperties); attribute.valueSpan, matchableAttributes, parsedProperties);
this.parseAssignmentEvent( this.parseAssignmentEvent(
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, attribute.valueSpan, bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, attribute.valueSpan,
matchableAttributes, boundEvents); matchableAttributes, boundEvents);
@ -340,7 +341,7 @@ class HtmlAstToIvyAst implements html.Visitor {
} else if (bindParts[IDENT_PROPERTY_IDX]) { } else if (bindParts[IDENT_PROPERTY_IDX]) {
this.bindingParser.parsePropertyBinding( this.bindingParser.parsePropertyBinding(
bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, absoluteOffset, bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, absoluteOffset,
matchableAttributes, parsedProperties); attribute.valueSpan, matchableAttributes, parsedProperties);
} else if (bindParts[IDENT_EVENT_IDX]) { } else if (bindParts[IDENT_EVENT_IDX]) {
const events: ParsedEvent[] = []; const events: ParsedEvent[] = [];
@ -351,7 +352,7 @@ class HtmlAstToIvyAst implements html.Visitor {
} }
} else { } else {
hasBinding = this.bindingParser.parsePropertyInterpolation( hasBinding = this.bindingParser.parsePropertyInterpolation(
name, value, srcSpan, matchableAttributes, parsedProperties); name, value, srcSpan, attribute.valueSpan, matchableAttributes, parsedProperties);
} }
return hasBinding; return hasBinding;
@ -365,20 +366,22 @@ class HtmlAstToIvyAst implements html.Visitor {
} }
private parseVariable( private parseVariable(
identifier: string, value: string, sourceSpan: ParseSourceSpan, variables: t.Variable[]) { identifier: string, value: string, sourceSpan: ParseSourceSpan,
valueSpan: ParseSourceSpan|undefined, variables: t.Variable[]) {
if (identifier.indexOf('-') > -1) { if (identifier.indexOf('-') > -1) {
this.reportError(`"-" is not allowed in variable names`, sourceSpan); this.reportError(`"-" is not allowed in variable names`, sourceSpan);
} }
variables.push(new t.Variable(identifier, value, sourceSpan)); variables.push(new t.Variable(identifier, value, sourceSpan, valueSpan));
} }
private parseReference( private parseReference(
identifier: string, value: string, sourceSpan: ParseSourceSpan, references: t.Reference[]) { identifier: string, value: string, sourceSpan: ParseSourceSpan,
valueSpan: ParseSourceSpan|undefined, references: t.Reference[]) {
if (identifier.indexOf('-') > -1) { if (identifier.indexOf('-') > -1) {
this.reportError(`"-" is not allowed in reference names`, sourceSpan); this.reportError(`"-" is not allowed in reference names`, sourceSpan);
} }
references.push(new t.Reference(identifier, value, sourceSpan)); references.push(new t.Reference(identifier, value, sourceSpan, valueSpan));
} }
private parseAssignmentEvent( private parseAssignmentEvent(

View File

@ -57,7 +57,8 @@ export class BindingParser {
const expression = dirMeta.hostProperties[propName]; const expression = dirMeta.hostProperties[propName];
if (typeof expression === 'string') { if (typeof expression === 'string') {
this.parsePropertyBinding( this.parsePropertyBinding(
propName, expression, true, sourceSpan, sourceSpan.start.offset, [], boundProps); propName, expression, true, sourceSpan, sourceSpan.start.offset, undefined, [],
boundProps);
} else { } else {
this._reportError( this._reportError(
`Value of the host property binding "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`, `Value of the host property binding "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
@ -125,11 +126,13 @@ export class BindingParser {
targetVars.push(new ParsedVariable(binding.key, binding.name, sourceSpan)); targetVars.push(new ParsedVariable(binding.key, binding.name, sourceSpan));
} else if (binding.expression) { } else if (binding.expression) {
this._parsePropertyAst( this._parsePropertyAst(
binding.key, binding.expression, sourceSpan, targetMatchableAttrs, targetProps); binding.key, binding.expression, sourceSpan, undefined, targetMatchableAttrs,
targetProps);
} else { } else {
targetMatchableAttrs.push([binding.key, '']); targetMatchableAttrs.push([binding.key, '']);
this.parseLiteralAttr( this.parseLiteralAttr(
binding.key, null, sourceSpan, absoluteOffset, targetMatchableAttrs, targetProps); binding.key, null, sourceSpan, absoluteOffset, undefined, targetMatchableAttrs,
targetProps);
} }
} }
} }
@ -158,7 +161,8 @@ export class BindingParser {
parseLiteralAttr( parseLiteralAttr(
name: string, value: string|null, sourceSpan: ParseSourceSpan, absoluteOffset: number, name: string, value: string|null, sourceSpan: ParseSourceSpan, absoluteOffset: number,
targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) { valueSpan: ParseSourceSpan|undefined, targetMatchableAttrs: string[][],
targetProps: ParsedProperty[]) {
if (isAnimationLabel(name)) { if (isAnimationLabel(name)) {
name = name.substring(1); name = name.substring(1);
if (value) { if (value) {
@ -168,17 +172,18 @@ export class BindingParser {
sourceSpan, ParseErrorLevel.ERROR); sourceSpan, ParseErrorLevel.ERROR);
} }
this._parseAnimation( this._parseAnimation(
name, value, sourceSpan, absoluteOffset, targetMatchableAttrs, targetProps); name, value, sourceSpan, absoluteOffset, valueSpan, targetMatchableAttrs, targetProps);
} else { } else {
targetProps.push(new ParsedProperty( targetProps.push(new ParsedProperty(
name, this._exprParser.wrapLiteralPrimitive(value, '', absoluteOffset), name, this._exprParser.wrapLiteralPrimitive(value, '', absoluteOffset),
ParsedPropertyType.LITERAL_ATTR, sourceSpan)); ParsedPropertyType.LITERAL_ATTR, sourceSpan, valueSpan));
} }
} }
parsePropertyBinding( parsePropertyBinding(
name: string, expression: string, isHost: boolean, sourceSpan: ParseSourceSpan, name: string, expression: string, isHost: boolean, sourceSpan: ParseSourceSpan,
absoluteOffset: number, targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) { absoluteOffset: number, valueSpan: ParseSourceSpan|undefined,
targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) {
let isAnimationProp = false; let isAnimationProp = false;
if (name.startsWith(ANIMATE_PROP_PREFIX)) { if (name.startsWith(ANIMATE_PROP_PREFIX)) {
isAnimationProp = true; isAnimationProp = true;
@ -190,20 +195,22 @@ export class BindingParser {
if (isAnimationProp) { if (isAnimationProp) {
this._parseAnimation( this._parseAnimation(
name, expression, sourceSpan, absoluteOffset, targetMatchableAttrs, targetProps); name, expression, sourceSpan, absoluteOffset, valueSpan, targetMatchableAttrs,
targetProps);
} else { } else {
this._parsePropertyAst( this._parsePropertyAst(
name, this._parseBinding(expression, isHost, sourceSpan, absoluteOffset), sourceSpan, name, this._parseBinding(expression, isHost, valueSpan || sourceSpan, absoluteOffset),
targetMatchableAttrs, targetProps); sourceSpan, valueSpan, targetMatchableAttrs, targetProps);
} }
} }
parsePropertyInterpolation( parsePropertyInterpolation(
name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][], name: string, value: string, sourceSpan: ParseSourceSpan,
valueSpan: ParseSourceSpan|undefined, targetMatchableAttrs: string[][],
targetProps: ParsedProperty[]): boolean { targetProps: ParsedProperty[]): boolean {
const expr = this.parseInterpolation(value, sourceSpan); const expr = this.parseInterpolation(value, valueSpan || sourceSpan);
if (expr) { if (expr) {
this._parsePropertyAst(name, expr, sourceSpan, targetMatchableAttrs, targetProps); this._parsePropertyAst(name, expr, sourceSpan, valueSpan, targetMatchableAttrs, targetProps);
return true; return true;
} }
return false; return false;
@ -211,20 +218,25 @@ export class BindingParser {
private _parsePropertyAst( private _parsePropertyAst(
name: string, ast: ASTWithSource, sourceSpan: ParseSourceSpan, name: string, ast: ASTWithSource, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) { valueSpan: ParseSourceSpan|undefined, targetMatchableAttrs: string[][],
targetProps: ParsedProperty[]) {
targetMatchableAttrs.push([name, ast.source !]); targetMatchableAttrs.push([name, ast.source !]);
targetProps.push(new ParsedProperty(name, ast, ParsedPropertyType.DEFAULT, sourceSpan)); targetProps.push(
new ParsedProperty(name, ast, ParsedPropertyType.DEFAULT, sourceSpan, valueSpan));
} }
private _parseAnimation( private _parseAnimation(
name: string, expression: string|null, sourceSpan: ParseSourceSpan, absoluteOffset: number, name: string, expression: string|null, sourceSpan: ParseSourceSpan, absoluteOffset: number,
targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) { valueSpan: ParseSourceSpan|undefined, targetMatchableAttrs: string[][],
targetProps: ParsedProperty[]) {
// This will occur when a @trigger is not paired with an expression. // This will occur when a @trigger is not paired with an expression.
// For animations it is valid to not have an expression since */void // For animations it is valid to not have an expression since */void
// states will be applied by angular when the element is attached/detached // states will be applied by angular when the element is attached/detached
const ast = this._parseBinding(expression || 'undefined', false, sourceSpan, absoluteOffset); const ast = this._parseBinding(
expression || 'undefined', false, valueSpan || sourceSpan, absoluteOffset);
targetMatchableAttrs.push([name, ast.source !]); targetMatchableAttrs.push([name, ast.source !]);
targetProps.push(new ParsedProperty(name, ast, ParsedPropertyType.ANIMATION, sourceSpan)); targetProps.push(
new ParsedProperty(name, ast, ParsedPropertyType.ANIMATION, sourceSpan, valueSpan));
} }
private _parseBinding( private _parseBinding(
@ -253,7 +265,7 @@ export class BindingParser {
if (boundProp.isAnimation) { if (boundProp.isAnimation) {
return new BoundElementProperty( return new BoundElementProperty(
boundProp.name, BindingType.Animation, SecurityContext.NONE, boundProp.expression, null, boundProp.name, BindingType.Animation, SecurityContext.NONE, boundProp.expression, null,
boundProp.sourceSpan); boundProp.sourceSpan, boundProp.valueSpan);
} }
let unit: string|null = null; let unit: string|null = null;
@ -306,7 +318,7 @@ export class BindingParser {
return new BoundElementProperty( return new BoundElementProperty(
boundPropertyName, bindingType, securityContexts[0], boundProp.expression, unit, boundPropertyName, bindingType, securityContexts[0], boundProp.expression, unit,
boundProp.sourceSpan); boundProp.sourceSpan, boundProp.valueSpan);
} }
parseEvent( parseEvent(

View File

@ -426,8 +426,8 @@ class TemplateParseVisitor implements html.Visitor {
hasBinding = true; hasBinding = true;
if (bindParts[KW_BIND_IDX] != null) { if (bindParts[KW_BIND_IDX] != null) {
this._bindingParser.parsePropertyBinding( this._bindingParser.parsePropertyBinding(
bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, targetMatchableAttrs, bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, attr.valueSpan,
targetProps); targetMatchableAttrs, targetProps);
} else if (bindParts[KW_LET_IDX]) { } else if (bindParts[KW_LET_IDX]) {
if (isTemplateElement) { if (isTemplateElement) {
@ -448,19 +448,20 @@ class TemplateParseVisitor implements html.Visitor {
} else if (bindParts[KW_BINDON_IDX]) { } else if (bindParts[KW_BINDON_IDX]) {
this._bindingParser.parsePropertyBinding( this._bindingParser.parsePropertyBinding(
bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, targetMatchableAttrs, bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, attr.valueSpan,
targetProps); targetMatchableAttrs, targetProps);
this._parseAssignmentEvent( this._parseAssignmentEvent(
bindParts[IDENT_KW_IDX], value, srcSpan, attr.valueSpan || srcSpan, bindParts[IDENT_KW_IDX], value, srcSpan, attr.valueSpan || srcSpan,
targetMatchableAttrs, boundEvents); targetMatchableAttrs, boundEvents);
} else if (bindParts[KW_AT_IDX]) { } else if (bindParts[KW_AT_IDX]) {
this._bindingParser.parseLiteralAttr( this._bindingParser.parseLiteralAttr(
name, value, srcSpan, absoluteOffset, targetMatchableAttrs, targetProps); name, value, srcSpan, absoluteOffset, attr.valueSpan, targetMatchableAttrs,
targetProps);
} else if (bindParts[IDENT_BANANA_BOX_IDX]) { } else if (bindParts[IDENT_BANANA_BOX_IDX]) {
this._bindingParser.parsePropertyBinding( this._bindingParser.parsePropertyBinding(
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, absoluteOffset, bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, absoluteOffset, attr.valueSpan,
targetMatchableAttrs, targetProps); targetMatchableAttrs, targetProps);
this._parseAssignmentEvent( this._parseAssignmentEvent(
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, attr.valueSpan || srcSpan, bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, attr.valueSpan || srcSpan,
@ -468,7 +469,7 @@ class TemplateParseVisitor implements html.Visitor {
} else if (bindParts[IDENT_PROPERTY_IDX]) { } else if (bindParts[IDENT_PROPERTY_IDX]) {
this._bindingParser.parsePropertyBinding( this._bindingParser.parsePropertyBinding(
bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, absoluteOffset, bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, absoluteOffset, attr.valueSpan,
targetMatchableAttrs, targetProps); targetMatchableAttrs, targetProps);
} else if (bindParts[IDENT_EVENT_IDX]) { } else if (bindParts[IDENT_EVENT_IDX]) {
@ -478,12 +479,12 @@ class TemplateParseVisitor implements html.Visitor {
} }
} else { } else {
hasBinding = this._bindingParser.parsePropertyInterpolation( hasBinding = this._bindingParser.parsePropertyInterpolation(
name, value, srcSpan, targetMatchableAttrs, targetProps); name, value, srcSpan, attr.valueSpan, targetMatchableAttrs, targetProps);
} }
if (!hasBinding) { if (!hasBinding) {
this._bindingParser.parseLiteralAttr( this._bindingParser.parseLiteralAttr(
name, value, srcSpan, absoluteOffset, targetMatchableAttrs, targetProps); name, value, srcSpan, absoluteOffset, attr.valueSpan, targetMatchableAttrs, targetProps);
} }
targetEvents.push(...boundEvents.map(e => t.BoundEventAst.fromParsedEvent(e))); targetEvents.push(...boundEvents.map(e => t.BoundEventAst.fromParsedEvent(e)));

View File

@ -0,0 +1,309 @@
/**
* @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 {ParseSourceSpan} from '../../src/parse_util';
import * as t from '../../src/render3/r3_ast';
import {parseR3 as parse} from './view/util';
class R3AstSourceSpans implements t.Visitor<void> {
result: any[] = [];
visitElement(element: t.Element) {
this.result.push([
'Element', humanizeSpan(element.sourceSpan), humanizeSpan(element.startSourceSpan),
humanizeSpan(element.endSourceSpan)
]);
this.visitAll([
element.attributes,
element.inputs,
element.outputs,
element.references,
element.children,
]);
}
visitTemplate(template: t.Template) {
this.result.push([
'Template', humanizeSpan(template.sourceSpan), humanizeSpan(template.startSourceSpan),
humanizeSpan(template.endSourceSpan)
]);
this.visitAll([
template.attributes,
template.inputs,
template.outputs,
template.templateAttrs,
template.references,
template.variables,
template.children,
]);
}
visitContent(content: t.Content) {
this.result.push(['Content', humanizeSpan(content.sourceSpan)]);
t.visitAll(this, content.attributes);
}
visitVariable(variable: t.Variable) {
this.result.push(
['Variable', humanizeSpan(variable.sourceSpan), humanizeSpan(variable.valueSpan)]);
}
visitReference(reference: t.Reference) {
this.result.push(
['Reference', humanizeSpan(reference.sourceSpan), humanizeSpan(reference.valueSpan)]);
}
visitTextAttribute(attribute: t.TextAttribute) {
this.result.push(
['TextAttribute', humanizeSpan(attribute.sourceSpan), humanizeSpan(attribute.valueSpan)]);
}
visitBoundAttribute(attribute: t.BoundAttribute) {
this.result.push(
['BoundAttribute', humanizeSpan(attribute.sourceSpan), humanizeSpan(attribute.valueSpan)]);
}
visitBoundEvent(event: t.BoundEvent) {
this.result.push(
['BoundEvent', humanizeSpan(event.sourceSpan), humanizeSpan(event.handlerSpan)]);
}
visitText(text: t.Text) { this.result.push(['Text', humanizeSpan(text.sourceSpan)]); }
visitBoundText(text: t.BoundText) {
this.result.push(['BoundText', humanizeSpan(text.sourceSpan)]);
}
visitIcu(icu: t.Icu) { return null; }
private visitAll(nodes: t.Node[][]) { nodes.forEach(node => t.visitAll(this, node)); }
}
function humanizeSpan(span: ParseSourceSpan | null | undefined): string {
if (span === null || span === undefined) {
return `<empty>`;
}
return `${span.start.offset}:${span.end.offset}`;
}
function expectFromHtml(html: string) {
const res = parse(html);
return expectFromR3Nodes(res.nodes);
}
function expectFromR3Nodes(nodes: t.Node[]) {
const humanizer = new R3AstSourceSpans();
t.visitAll(humanizer, nodes);
return expect(humanizer.result);
}
describe('R3 AST source spans', () => {
describe('nodes without binding', () => {
it('is correct for text nodes', () => {
expectFromHtml('a').toEqual([
['Text', '0:1'],
]);
});
it('is correct for elements with attributes', () => {
expectFromHtml('<div a="b"></div>').toEqual([
['Element', '0:17', '0:11', '11:17'],
['TextAttribute', '5:10', '8:9'],
]);
});
it('is correct for elements with attributes without value', () => {
expectFromHtml('<div a></div>').toEqual([
['Element', '0:13', '0:7', '7:13'],
['TextAttribute', '5:6', '<empty>'],
]);
});
});
describe('bound text nodes', () => {
it('is correct for bound text nodes', () => {
expectFromHtml('{{a}}').toEqual([
['BoundText', '0:5'],
]);
});
});
describe('bound attributes', () => {
it('is correct for bound properties', () => {
expectFromHtml('<div [someProp]="v"></div>').toEqual([
['Element', '0:26', '0:20', '20:26'],
['BoundAttribute', '5:19', '17:18'],
]);
});
it('is correct for bound properties without value', () => {
expectFromHtml('<div [someProp]></div>').toEqual([
['Element', '0:22', '0:16', '16:22'],
['BoundAttribute', '5:15', '<empty>'],
]);
});
it('is correct for bound properties via bind- ', () => {
expectFromHtml('<div bind-prop="v"></div>').toEqual([
['Element', '0:25', '0:19', '19:25'],
['BoundAttribute', '5:18', '16:17'],
]);
});
it('is correct for bound properties via {{...}}', () => {
expectFromHtml('<div prop="{{v}}"></div>').toEqual([
['Element', '0:24', '0:18', '18:24'],
['BoundAttribute', '5:17', '11:16'],
]);
});
});
describe('templates', () => {
it('is correct for * directives', () => {
expectFromHtml('<div *ngIf></div>').toEqual([
['Template', '0:11', '0:11', '11:17'],
['TextAttribute', '5:10', '<empty>'],
['Element', '0:17', '0:11', '11:17'],
]);
});
it('is correct for <ng-template>', () => {
expectFromHtml('<ng-template></ng-template>').toEqual([
['Template', '0:13', '0:13', '13:27'],
]);
});
it('is correct for reference via #...', () => {
expectFromHtml('<ng-template #a></ng-template>').toEqual([
['Template', '0:16', '0:16', '16:30'],
['Reference', '13:15', '<empty>'],
]);
});
it('is correct for reference with name', () => {
expectFromHtml('<ng-template #a="b"></ng-template>').toEqual([
['Template', '0:20', '0:20', '20:34'],
['Reference', '13:19', '17:18'],
]);
});
it('is correct for reference via ref-...', () => {
expectFromHtml('<ng-template ref-a></ng-template>').toEqual([
['Template', '0:19', '0:19', '19:33'],
['Reference', '13:18', '<empty>'],
]);
});
it('is correct for variables via let-...', () => {
expectFromHtml('<ng-template let-a="b"></ng-template>').toEqual([
['Template', '0:23', '0:23', '23:37'],
['Variable', '13:22', '20:21'],
]);
});
it('is correct for attributes', () => {
expectFromHtml('<ng-template k1="v1"></ng-template>').toEqual([
['Template', '0:21', '0:21', '21:35'],
['TextAttribute', '13:20', '17:19'],
]);
});
it('is correct for bound attributes', () => {
expectFromHtml('<ng-template [k1]="v1"></ng-template>').toEqual([
['Template', '0:23', '0:23', '23:37'],
['BoundAttribute', '13:22', '19:21'],
]);
});
});
// TODO(joost): improve spans of nodes extracted from macrosyntax
describe('inline templates', () => {
it('is correct for attribute and bound attributes', () => {
expectFromHtml('<div *ngFor="item of items"></div>').toEqual([
['Template', '0:28', '0:28', '28:34'],
['BoundAttribute', '5:27', '<empty>'],
['BoundAttribute', '5:27', '<empty>'],
['Element', '0:34', '0:28', '28:34'],
]);
});
it('is correct for variables via let ...', () => {
expectFromHtml('<div *ngIf="let a=b"></div>').toEqual([
['Template', '0:21', '0:21', '21:27'],
['TextAttribute', '5:20', '<empty>'],
['Variable', '5:20', '<empty>'],
['Element', '0:27', '0:21', '21:27'],
]);
});
it('is correct for variables via as ...', () => {
expectFromHtml('<div *ngIf="expr as local"></div>').toEqual([
['Template', '0:27', '0:27', '27:33'],
['BoundAttribute', '5:26', '<empty>'],
['Variable', '5:26', '<empty>'],
['Element', '0:33', '0:27', '27:33'],
]);
});
});
describe('events', () => {
it('is correct for event names case sensitive', () => {
expectFromHtml('<div (someEvent)="v"></div>').toEqual([
['Element', '0:27', '0:21', '21:27'],
['BoundEvent', '5:20', '18:19'],
]);
});
it('is correct for bound events via on-', () => {
expectFromHtml('<div on-event="v"></div>').toEqual([
['Element', '0:24', '0:18', '18:24'],
['BoundEvent', '5:17', '15:16'],
]);
});
it('is correct for bound events and properties via [(...)]', () => {
expectFromHtml('<div [(prop)]="v"></div>').toEqual([
['Element', '0:24', '0:18', '18:24'],
['BoundAttribute', '5:17', '15:16'],
['BoundEvent', '5:17', '15:16'],
]);
});
it('is correct for bound events and properties via bindon-', () => {
expectFromHtml('<div bindon-prop="v"></div>').toEqual([
['Element', '0:27', '0:21', '21:27'],
['BoundAttribute', '5:20', '18:19'],
['BoundEvent', '5:20', '18:19'],
]);
});
});
describe('references', () => {
it('is correct for references via #...', () => {
expectFromHtml('<div #a></div>').toEqual([
['Element', '0:14', '0:8', '8:14'],
['Reference', '5:7', '<empty>'],
]);
});
it('is correct for references with name', () => {
expectFromHtml('<div #a="b"></div>').toEqual([
['Element', '0:18', '0:12', '12:18'],
['Reference', '5:11', '9:10'],
]);
});
it('is correct for references via ref-', () => {
expectFromHtml('<div ref-a></div>').toEqual([
['Element', '0:17', '0:11', '11:17'],
['Reference', '5:10', '<empty>'],
]);
});
});
});

View File

@ -1888,7 +1888,7 @@ Can't bind to 'invalidProp' since it isn't a known property of 'div'. ("[ERROR -
it('should report errors in expressions', () => { it('should report errors in expressions', () => {
expect(() => parse('<div [prop]="a b"></div>', [])).toThrowError(`Template parse errors: expect(() => parse('<div [prop]="a b"></div>', [])).toThrowError(`Template parse errors:
Parser Error: Unexpected token 'b' at column 3 in [a b] in TestComp@0:5 ("<div [ERROR ->][prop]="a b"></div>"): TestComp@0:5`); Parser Error: Unexpected token 'b' at column 3 in [a b] in TestComp@0:13 ("<div [prop]="[ERROR ->]a b"></div>"): TestComp@0:13`);
}); });
it('should not throw on invalid property names if the property is used by a directive', it('should not throw on invalid property names if the property is used by a directive',