fix(compiler): ensure that event handlers have the correct source spans (#28055)

When template bindings are being parsed the event handlers
were receiving a source span that included the whole attribute.

Now they get a span that is focussed on the handler itself.

PR Close #28055
This commit is contained in:
Pete Bacon Darwin 2019-02-08 22:10:20 +00:00 committed by Misko Hevery
parent 497619f25d
commit cffd86260a
8 changed files with 58 additions and 32 deletions

View File

@ -698,7 +698,8 @@ export class ParsedEvent {
// Animation events have a phase // Animation events have a phase
constructor( constructor(
public name: string, public targetOrPhase: string, public type: ParsedEventType, public name: string, public targetOrPhase: string, public type: ParsedEventType,
public handler: AST, public sourceSpan: ParseSourceSpan) {} public handler: AST, public sourceSpan: ParseSourceSpan,
public handlerSpan: ParseSourceSpan) {}
} }
export class ParsedVariable { export class ParsedVariable {

View File

@ -50,13 +50,15 @@ export class BoundAttribute implements Node {
export class BoundEvent implements Node { export class BoundEvent implements Node {
constructor( constructor(
public name: string, public type: ParsedEventType, public handler: AST, public name: string, public type: ParsedEventType, public handler: AST,
public target: string|null, public phase: string|null, public sourceSpan: ParseSourceSpan) {} public target: string|null, public phase: string|null, public sourceSpan: ParseSourceSpan,
public handlerSpan: ParseSourceSpan) {}
static fromParsedEvent(event: ParsedEvent) { static fromParsedEvent(event: ParsedEvent) {
const target: string|null = event.type === ParsedEventType.Regular ? event.targetOrPhase : null; const target: string|null = event.type === ParsedEventType.Regular ? event.targetOrPhase : null;
const phase: string|null = const phase: string|null =
event.type === ParsedEventType.Animation ? event.targetOrPhase : null; event.type === ParsedEventType.Animation ? event.targetOrPhase : null;
return new BoundEvent(event.name, event.type, event.handler, target, phase, event.sourceSpan); return new BoundEvent(
event.name, event.type, event.handler, target, phase, event.sourceSpan, event.handlerSpan);
} }
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitBoundEvent(this); } visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitBoundEvent(this); }
@ -67,7 +69,12 @@ export class Element implements Node {
public name: string, public attributes: TextAttribute[], public inputs: BoundAttribute[], public name: string, public attributes: TextAttribute[], public inputs: BoundAttribute[],
public outputs: BoundEvent[], public children: Node[], public references: Reference[], public outputs: BoundEvent[], public children: Node[], public references: Reference[],
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null, public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null,
public endSourceSpan: ParseSourceSpan|null, public i18n?: I18nAST) {} public endSourceSpan: ParseSourceSpan|null, public i18n?: I18nAST) {
// If the element is empty then the source span should include any closing tag
if (children.length === 0 && startSourceSpan && endSourceSpan) {
this.sourceSpan = {...sourceSpan, end: endSourceSpan.end};
}
}
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitElement(this); } visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitElement(this); }
} }

View File

@ -284,13 +284,15 @@ class HtmlAstToIvyAst implements html.Visitor {
} else if (bindParts[KW_ON_IDX]) { } else if (bindParts[KW_ON_IDX]) {
const events: ParsedEvent[] = []; const events: ParsedEvent[] = [];
this.bindingParser.parseEvent( this.bindingParser.parseEvent(
bindParts[IDENT_KW_IDX], value, srcSpan, matchableAttributes, events); bindParts[IDENT_KW_IDX], value, srcSpan, attribute.valueSpan || srcSpan,
matchableAttributes, events);
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, matchableAttributes, parsedProperties); bindParts[IDENT_KW_IDX], value, false, srcSpan, matchableAttributes, parsedProperties);
this.parseAssignmentEvent( this.parseAssignmentEvent(
bindParts[IDENT_KW_IDX], value, srcSpan, matchableAttributes, boundEvents); bindParts[IDENT_KW_IDX], value, srcSpan, attribute.valueSpan, matchableAttributes,
boundEvents);
} else if (bindParts[KW_AT_IDX]) { } else if (bindParts[KW_AT_IDX]) {
this.bindingParser.parseLiteralAttr( this.bindingParser.parseLiteralAttr(
name, value, srcSpan, matchableAttributes, parsedProperties); name, value, srcSpan, matchableAttributes, parsedProperties);
@ -300,7 +302,8 @@ class HtmlAstToIvyAst implements html.Visitor {
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, matchableAttributes, bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, matchableAttributes,
parsedProperties); parsedProperties);
this.parseAssignmentEvent( this.parseAssignmentEvent(
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, matchableAttributes, boundEvents); bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, attribute.valueSpan,
matchableAttributes, boundEvents);
} else if (bindParts[IDENT_PROPERTY_IDX]) { } else if (bindParts[IDENT_PROPERTY_IDX]) {
this.bindingParser.parsePropertyBinding( this.bindingParser.parsePropertyBinding(
@ -310,7 +313,8 @@ class HtmlAstToIvyAst implements html.Visitor {
} else if (bindParts[IDENT_EVENT_IDX]) { } else if (bindParts[IDENT_EVENT_IDX]) {
const events: ParsedEvent[] = []; const events: ParsedEvent[] = [];
this.bindingParser.parseEvent( this.bindingParser.parseEvent(
bindParts[IDENT_EVENT_IDX], value, srcSpan, matchableAttributes, events); bindParts[IDENT_EVENT_IDX], value, srcSpan, attribute.valueSpan || srcSpan,
matchableAttributes, events);
addEvents(events, boundEvents); addEvents(events, boundEvents);
} }
} else { } else {
@ -347,10 +351,12 @@ class HtmlAstToIvyAst implements html.Visitor {
private parseAssignmentEvent( private parseAssignmentEvent(
name: string, expression: string, sourceSpan: ParseSourceSpan, name: string, expression: string, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], boundEvents: t.BoundEvent[]) { valueSpan: ParseSourceSpan|undefined, targetMatchableAttrs: string[][],
boundEvents: t.BoundEvent[]) {
const events: ParsedEvent[] = []; const events: ParsedEvent[] = [];
this.bindingParser.parseEvent( this.bindingParser.parseEvent(
`${name}Change`, `${expression}=$event`, sourceSpan, targetMatchableAttrs, events); `${name}Change`, `${expression}=$event`, sourceSpan, valueSpan || sourceSpan,
targetMatchableAttrs, events);
addEvents(events, boundEvents); addEvents(events, boundEvents);
} }

View File

@ -79,7 +79,8 @@ export function prepareEventListenerParameters(
} }
const bindingExpr = convertActionBinding( const bindingExpr = convertActionBinding(
scope, bindingContext, handler, 'b', () => error('Unexpected interpolation')); scope, bindingContext, handler, 'b', () => error('Unexpected interpolation'),
eventAst.handlerSpan);
const statements = []; const statements = [];
if (scope) { if (scope) {

View File

@ -83,7 +83,8 @@ export class BindingParser {
Object.keys(dirMeta.hostListeners).forEach(propName => { Object.keys(dirMeta.hostListeners).forEach(propName => {
const expression = dirMeta.hostListeners[propName]; const expression = dirMeta.hostListeners[propName];
if (typeof expression === 'string') { if (typeof expression === 'string') {
this.parseEvent(propName, expression, sourceSpan, [], targetEvents); // TODO: pass a more accurate handlerSpan for this event.
this.parseEvent(propName, expression, sourceSpan, sourceSpan, [], targetEvents);
} else { } else {
this._reportError( this._reportError(
`Value of the host listener "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`, `Value of the host listener "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
@ -300,13 +301,14 @@ export class BindingParser {
} }
parseEvent( parseEvent(
name: string, expression: string, sourceSpan: ParseSourceSpan, name: string, expression: string, sourceSpan: ParseSourceSpan, handlerSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) { targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) {
if (isAnimationLabel(name)) { if (isAnimationLabel(name)) {
name = name.substr(1); name = name.substr(1);
this._parseAnimationEvent(name, expression, sourceSpan, targetEvents); this._parseAnimationEvent(name, expression, sourceSpan, handlerSpan, targetEvents);
} else { } else {
this._parseRegularEvent(name, expression, sourceSpan, targetMatchableAttrs, targetEvents); this._parseRegularEvent(
name, expression, sourceSpan, handlerSpan, targetMatchableAttrs, targetEvents);
} }
} }
@ -317,7 +319,8 @@ export class BindingParser {
} }
private _parseAnimationEvent( private _parseAnimationEvent(
name: string, expression: string, sourceSpan: ParseSourceSpan, targetEvents: ParsedEvent[]) { name: string, expression: string, sourceSpan: ParseSourceSpan, handlerSpan: ParseSourceSpan,
targetEvents: ParsedEvent[]) {
const matches = splitAtPeriod(name, [name, '']); const matches = splitAtPeriod(name, [name, '']);
const eventName = matches[0]; const eventName = matches[0];
const phase = matches[1].toLowerCase(); const phase = matches[1].toLowerCase();
@ -325,9 +328,9 @@ export class BindingParser {
switch (phase) { switch (phase) {
case 'start': case 'start':
case 'done': case 'done':
const ast = this._parseAction(expression, sourceSpan); const ast = this._parseAction(expression, handlerSpan);
targetEvents.push( targetEvents.push(new ParsedEvent(
new ParsedEvent(eventName, phase, ParsedEventType.Animation, ast, sourceSpan)); eventName, phase, ParsedEventType.Animation, ast, sourceSpan, handlerSpan));
break; break;
default: default:
@ -344,13 +347,14 @@ export class BindingParser {
} }
private _parseRegularEvent( private _parseRegularEvent(
name: string, expression: string, sourceSpan: ParseSourceSpan, name: string, expression: string, sourceSpan: ParseSourceSpan, handlerSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) { targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) {
// long format: 'target: eventName' // long format: 'target: eventName'
const [target, eventName] = splitAtColon(name, [null !, name]); const [target, eventName] = splitAtColon(name, [null !, name]);
const ast = this._parseAction(expression, sourceSpan); const ast = this._parseAction(expression, handlerSpan);
targetMatchableAttrs.push([name !, ast.source !]); targetMatchableAttrs.push([name !, ast.source !]);
targetEvents.push(new ParsedEvent(eventName, target, ParsedEventType.Regular, ast, sourceSpan)); targetEvents.push(
new ParsedEvent(eventName, target, ParsedEventType.Regular, ast, sourceSpan, handlerSpan));
// Don't detect directives for event names for now, // Don't detect directives for event names for now,
// so don't add the event name to the matchableAttrs // so don't add the event name to the matchableAttrs
} }

View File

@ -114,7 +114,8 @@ export class BoundEventAst implements TemplateAst {
constructor( constructor(
public name: string, public target: string|null, public phase: string|null, public name: string, public target: string|null, public phase: string|null,
public handler: AST, public sourceSpan: ParseSourceSpan) { public handler: AST, public sourceSpan: ParseSourceSpan,
public handlerSpan: ParseSourceSpan) {
this.fullName = BoundEventAst.calcFullName(this.name, this.target, this.phase); this.fullName = BoundEventAst.calcFullName(this.name, this.target, this.phase);
this.isAnimation = !!this.phase; this.isAnimation = !!this.phase;
} }
@ -134,7 +135,8 @@ export class BoundEventAst implements TemplateAst {
const target: string|null = event.type === ParsedEventType.Regular ? event.targetOrPhase : null; const target: string|null = event.type === ParsedEventType.Regular ? event.targetOrPhase : null;
const phase: string|null = const phase: string|null =
event.type === ParsedEventType.Animation ? event.targetOrPhase : null; event.type === ParsedEventType.Animation ? event.targetOrPhase : null;
return new BoundEventAst(event.name, target, phase, event.handler, event.sourceSpan); return new BoundEventAst(
event.name, target, phase, event.handler, event.sourceSpan, event.handlerSpan);
} }
visit(visitor: TemplateAstVisitor, context: any): any { visit(visitor: TemplateAstVisitor, context: any): any {

View File

@ -441,13 +441,15 @@ class TemplateParseVisitor implements html.Visitor {
} else if (bindParts[KW_ON_IDX]) { } else if (bindParts[KW_ON_IDX]) {
this._bindingParser.parseEvent( this._bindingParser.parseEvent(
bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, boundEvents); bindParts[IDENT_KW_IDX], value, srcSpan, attr.valueSpan || srcSpan,
targetMatchableAttrs, 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, targetMatchableAttrs, targetProps); bindParts[IDENT_KW_IDX], value, false, srcSpan, targetMatchableAttrs, targetProps);
this._parseAssignmentEvent( this._parseAssignmentEvent(
bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, boundEvents); bindParts[IDENT_KW_IDX], value, srcSpan, attr.valueSpan || srcSpan,
targetMatchableAttrs, boundEvents);
} else if (bindParts[KW_AT_IDX]) { } else if (bindParts[KW_AT_IDX]) {
this._bindingParser.parseLiteralAttr( this._bindingParser.parseLiteralAttr(
@ -458,7 +460,8 @@ class TemplateParseVisitor implements html.Visitor {
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, targetMatchableAttrs, bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, targetMatchableAttrs,
targetProps); targetProps);
this._parseAssignmentEvent( this._parseAssignmentEvent(
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, targetMatchableAttrs, boundEvents); bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, attr.valueSpan || srcSpan,
targetMatchableAttrs, boundEvents);
} else if (bindParts[IDENT_PROPERTY_IDX]) { } else if (bindParts[IDENT_PROPERTY_IDX]) {
this._bindingParser.parsePropertyBinding( this._bindingParser.parsePropertyBinding(
@ -467,7 +470,8 @@ class TemplateParseVisitor implements html.Visitor {
} else if (bindParts[IDENT_EVENT_IDX]) { } else if (bindParts[IDENT_EVENT_IDX]) {
this._bindingParser.parseEvent( this._bindingParser.parseEvent(
bindParts[IDENT_EVENT_IDX], value, srcSpan, targetMatchableAttrs, boundEvents); bindParts[IDENT_EVENT_IDX], value, srcSpan, attr.valueSpan || srcSpan,
targetMatchableAttrs, boundEvents);
} }
} else { } else {
hasBinding = this._bindingParser.parsePropertyInterpolation( hasBinding = this._bindingParser.parsePropertyInterpolation(
@ -507,10 +511,11 @@ class TemplateParseVisitor implements html.Visitor {
} }
private _parseAssignmentEvent( private _parseAssignmentEvent(
name: string, expression: string, sourceSpan: ParseSourceSpan, name: string, expression: string, sourceSpan: ParseSourceSpan, valueSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) { targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) {
this._bindingParser.parseEvent( this._bindingParser.parseEvent(
`${name}Change`, `${expression}=$event`, sourceSpan, targetMatchableAttrs, targetEvents); `${name}Change`, `${expression}=$event`, sourceSpan, valueSpan, targetMatchableAttrs,
targetEvents);
} }
private _parseDirectives(selectorMatcher: SelectorMatcher, elementCssSelector: CssSelector): private _parseDirectives(selectorMatcher: SelectorMatcher, elementCssSelector: CssSelector):

View File

@ -420,7 +420,7 @@ class ArrayConsole implements Console {
expectVisitedNode( expectVisitedNode(
new class extends new class extends
NullVisitor{visitEvent(ast: BoundEventAst, context: any): any{return ast;}}, NullVisitor{visitEvent(ast: BoundEventAst, context: any): any{return ast;}},
new BoundEventAst('foo', 'bar', 'goo', null !, null !)); new BoundEventAst('foo', 'bar', 'goo', null !, null !, null !));
}); });
it('should visit BoundElementPropertyAst', () => { it('should visit BoundElementPropertyAst', () => {
@ -474,7 +474,7 @@ class ArrayConsole implements Console {
new EmbeddedTemplateAst([], [], [], [], [], [], false, [], [], 0, null !), new EmbeddedTemplateAst([], [], [], [], [], [], false, [], [], 0, null !),
new ElementAst('foo', [], [], [], [], [], [], false, [], [], 0, null !, null !), new ElementAst('foo', [], [], [], [], [], [], false, [], [], 0, null !, null !),
new ReferenceAst('foo', null !, 'bar', null !), new VariableAst('foo', 'bar', null !), new ReferenceAst('foo', null !, 'bar', null !), new VariableAst('foo', 'bar', null !),
new BoundEventAst('foo', 'bar', 'goo', null !, null !), new BoundEventAst('foo', 'bar', 'goo', null !, null !, null !),
new BoundElementPropertyAst('foo', null !, null !, null !, 'bar', null !), new BoundElementPropertyAst('foo', null !, null !, null !, 'bar', null !),
new AttrAst('foo', 'bar', null !), new BoundTextAst(null !, 0, null !), new AttrAst('foo', 'bar', null !), new BoundTextAst(null !, 0, null !),
new TextAst('foo', 0, null !), new DirectiveAst(null !, [], [], [], 0, null !), new TextAst('foo', 0, null !), new DirectiveAst(null !, [], [], [], 0, null !),