diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index af70c38c12..70dd1f829a 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -52,14 +52,14 @@ export function generateTypeCheckBlock( // the `ts.Printer` to format the type-check block nicely. const body = ts.createBlock([ts.createIf(ts.createTrue(), innerBody, undefined)]); const fnDecl = ts.createFunctionDeclaration( - /* decorators */ undefined, - /* modifiers */ undefined, - /* asteriskToken */ undefined, - /* name */ name, - /* typeParameters */ ref.node.typeParameters, - /* parameters */ paramList, - /* type */ undefined, - /* body */ body); + /* decorators */ undefined, + /* modifiers */ undefined, + /* asteriskToken */ undefined, + /* name */ name, + /* typeParameters */ ref.node.typeParameters, + /* parameters */ paramList, + /* type */ undefined, + /* body */ body); addSourceInfo(fnDecl, ref.node); return fnDecl; } @@ -198,7 +198,9 @@ class TcbTemplateBodyOp extends TcbOp { i instanceof TmplAstBoundAttribute && i.name === guard.inputName); if (boundInput !== undefined) { // 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') { // Use the binding expression itself as guard. @@ -335,7 +337,8 @@ class TcbUnclaimedInputsOp extends TcbOp { 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 // the assignment. @@ -768,7 +771,7 @@ function tcbGetInputBindingExpressions( function processAttribute(attr: TmplAstBoundAttribute | TmplAstTextAttribute): void { if (attr instanceof TmplAstBoundAttribute && propMatch.has(attr.name)) { // 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. bindings.push({ property: attr.name, diff --git a/packages/compiler/src/expression_parser/ast.ts b/packages/compiler/src/expression_parser/ast.ts index 545c9e2a21..084de03945 100644 --- a/packages/compiler/src/expression_parser/ast.ts +++ b/packages/compiler/src/expression_parser/ast.ts @@ -691,7 +691,7 @@ export class ParsedProperty { constructor( 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.isAnimation = this.type === ParsedPropertyType.ANIMATION; } @@ -739,5 +739,6 @@ export const enum BindingType { export class BoundElementProperty { constructor( 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) {} } diff --git a/packages/compiler/src/render3/r3_ast.ts b/packages/compiler/src/render3/r3_ast.ts index fbe1c7b72f..cb13335dc6 100644 --- a/packages/compiler/src/render3/r3_ast.ts +++ b/packages/compiler/src/render3/r3_ast.ts @@ -37,11 +37,12 @@ export class BoundAttribute implements Node { constructor( public name: string, public type: BindingType, public securityContext: SecurityContext, 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) { 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(visitor: Visitor): Result { return visitor.visitBoundAttribute(this); } @@ -96,12 +97,16 @@ export class Content 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(visitor: Visitor): Result { return visitor.visitVariable(this); } } 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(visitor: Visitor): Result { return visitor.visitReference(this); } } diff --git a/packages/compiler/src/render3/r3_template_transform.ts b/packages/compiler/src/render3/r3_template_transform.ts index ab22c7d2aa..67e30b0e72 100644 --- a/packages/compiler/src/render3/r3_template_transform.ts +++ b/packages/compiler/src/render3/r3_template_transform.ts @@ -297,20 +297,20 @@ class HtmlAstToIvyAst implements html.Visitor { hasBinding = true; if (bindParts[KW_BIND_IDX] != null) { this.bindingParser.parsePropertyBinding( - bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, matchableAttributes, - parsedProperties); + bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, attribute.valueSpan, + matchableAttributes, parsedProperties); } else if (bindParts[KW_LET_IDX]) { if (isTemplateElement) { const identifier = bindParts[IDENT_KW_IDX]; - this.parseVariable(identifier, value, srcSpan, variables); + this.parseVariable(identifier, value, srcSpan, attribute.valueSpan, variables); } else { this.reportError(`"let-" is only supported on ng-template elements.`, srcSpan); } } else if (bindParts[KW_REF_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]) { const events: ParsedEvent[] = []; @@ -320,19 +320,20 @@ class HtmlAstToIvyAst implements html.Visitor { addEvents(events, boundEvents); } else if (bindParts[KW_BINDON_IDX]) { this.bindingParser.parsePropertyBinding( - bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, matchableAttributes, - parsedProperties); + bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, attribute.valueSpan, + matchableAttributes, parsedProperties); this.parseAssignmentEvent( bindParts[IDENT_KW_IDX], value, srcSpan, attribute.valueSpan, matchableAttributes, boundEvents); } else if (bindParts[KW_AT_IDX]) { this.bindingParser.parseLiteralAttr( - name, value, srcSpan, absoluteOffset, matchableAttributes, parsedProperties); + name, value, srcSpan, absoluteOffset, attribute.valueSpan, matchableAttributes, + parsedProperties); } else if (bindParts[IDENT_BANANA_BOX_IDX]) { this.bindingParser.parsePropertyBinding( bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, absoluteOffset, - matchableAttributes, parsedProperties); + attribute.valueSpan, matchableAttributes, parsedProperties); this.parseAssignmentEvent( bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, attribute.valueSpan, matchableAttributes, boundEvents); @@ -340,7 +341,7 @@ class HtmlAstToIvyAst implements html.Visitor { } else if (bindParts[IDENT_PROPERTY_IDX]) { this.bindingParser.parsePropertyBinding( bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, absoluteOffset, - matchableAttributes, parsedProperties); + attribute.valueSpan, matchableAttributes, parsedProperties); } else if (bindParts[IDENT_EVENT_IDX]) { const events: ParsedEvent[] = []; @@ -351,7 +352,7 @@ class HtmlAstToIvyAst implements html.Visitor { } } else { hasBinding = this.bindingParser.parsePropertyInterpolation( - name, value, srcSpan, matchableAttributes, parsedProperties); + name, value, srcSpan, attribute.valueSpan, matchableAttributes, parsedProperties); } return hasBinding; @@ -365,20 +366,22 @@ class HtmlAstToIvyAst implements html.Visitor { } 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) { 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( - 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) { 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( diff --git a/packages/compiler/src/template_parser/binding_parser.ts b/packages/compiler/src/template_parser/binding_parser.ts index 999197252f..b2e52327ab 100644 --- a/packages/compiler/src/template_parser/binding_parser.ts +++ b/packages/compiler/src/template_parser/binding_parser.ts @@ -57,7 +57,8 @@ export class BindingParser { const expression = dirMeta.hostProperties[propName]; if (typeof expression === 'string') { this.parsePropertyBinding( - propName, expression, true, sourceSpan, sourceSpan.start.offset, [], boundProps); + propName, expression, true, sourceSpan, sourceSpan.start.offset, undefined, [], + boundProps); } else { this._reportError( `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)); } else if (binding.expression) { this._parsePropertyAst( - binding.key, binding.expression, sourceSpan, targetMatchableAttrs, targetProps); + binding.key, binding.expression, sourceSpan, undefined, targetMatchableAttrs, + targetProps); } else { targetMatchableAttrs.push([binding.key, '']); 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( name: string, value: string|null, sourceSpan: ParseSourceSpan, absoluteOffset: number, - targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) { + valueSpan: ParseSourceSpan|undefined, targetMatchableAttrs: string[][], + targetProps: ParsedProperty[]) { if (isAnimationLabel(name)) { name = name.substring(1); if (value) { @@ -168,17 +172,18 @@ export class BindingParser { sourceSpan, ParseErrorLevel.ERROR); } this._parseAnimation( - name, value, sourceSpan, absoluteOffset, targetMatchableAttrs, targetProps); + name, value, sourceSpan, absoluteOffset, valueSpan, targetMatchableAttrs, targetProps); } else { targetProps.push(new ParsedProperty( name, this._exprParser.wrapLiteralPrimitive(value, '', absoluteOffset), - ParsedPropertyType.LITERAL_ATTR, sourceSpan)); + ParsedPropertyType.LITERAL_ATTR, sourceSpan, valueSpan)); } } parsePropertyBinding( 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; if (name.startsWith(ANIMATE_PROP_PREFIX)) { isAnimationProp = true; @@ -190,20 +195,22 @@ export class BindingParser { if (isAnimationProp) { this._parseAnimation( - name, expression, sourceSpan, absoluteOffset, targetMatchableAttrs, targetProps); + name, expression, sourceSpan, absoluteOffset, valueSpan, targetMatchableAttrs, + targetProps); } else { this._parsePropertyAst( - name, this._parseBinding(expression, isHost, sourceSpan, absoluteOffset), sourceSpan, - targetMatchableAttrs, targetProps); + name, this._parseBinding(expression, isHost, valueSpan || sourceSpan, absoluteOffset), + sourceSpan, valueSpan, targetMatchableAttrs, targetProps); } } parsePropertyInterpolation( - name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][], + name: string, value: string, sourceSpan: ParseSourceSpan, + valueSpan: ParseSourceSpan|undefined, targetMatchableAttrs: string[][], targetProps: ParsedProperty[]): boolean { - const expr = this.parseInterpolation(value, sourceSpan); + const expr = this.parseInterpolation(value, valueSpan || sourceSpan); if (expr) { - this._parsePropertyAst(name, expr, sourceSpan, targetMatchableAttrs, targetProps); + this._parsePropertyAst(name, expr, sourceSpan, valueSpan, targetMatchableAttrs, targetProps); return true; } return false; @@ -211,20 +218,25 @@ export class BindingParser { private _parsePropertyAst( name: string, ast: ASTWithSource, sourceSpan: ParseSourceSpan, - targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) { + valueSpan: ParseSourceSpan|undefined, targetMatchableAttrs: string[][], + targetProps: ParsedProperty[]) { 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( 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. // For animations it is valid to not have an expression since */void // 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 !]); - targetProps.push(new ParsedProperty(name, ast, ParsedPropertyType.ANIMATION, sourceSpan)); + targetProps.push( + new ParsedProperty(name, ast, ParsedPropertyType.ANIMATION, sourceSpan, valueSpan)); } private _parseBinding( @@ -253,7 +265,7 @@ export class BindingParser { if (boundProp.isAnimation) { return new BoundElementProperty( boundProp.name, BindingType.Animation, SecurityContext.NONE, boundProp.expression, null, - boundProp.sourceSpan); + boundProp.sourceSpan, boundProp.valueSpan); } let unit: string|null = null; @@ -306,7 +318,7 @@ export class BindingParser { return new BoundElementProperty( boundPropertyName, bindingType, securityContexts[0], boundProp.expression, unit, - boundProp.sourceSpan); + boundProp.sourceSpan, boundProp.valueSpan); } parseEvent( diff --git a/packages/compiler/src/template_parser/template_parser.ts b/packages/compiler/src/template_parser/template_parser.ts index a1de634d35..29bf328be5 100644 --- a/packages/compiler/src/template_parser/template_parser.ts +++ b/packages/compiler/src/template_parser/template_parser.ts @@ -426,8 +426,8 @@ class TemplateParseVisitor implements html.Visitor { hasBinding = true; if (bindParts[KW_BIND_IDX] != null) { this._bindingParser.parsePropertyBinding( - bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, targetMatchableAttrs, - targetProps); + bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, attr.valueSpan, + targetMatchableAttrs, targetProps); } else if (bindParts[KW_LET_IDX]) { if (isTemplateElement) { @@ -448,19 +448,20 @@ class TemplateParseVisitor implements html.Visitor { } else if (bindParts[KW_BINDON_IDX]) { this._bindingParser.parsePropertyBinding( - bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, targetMatchableAttrs, - targetProps); + bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, attr.valueSpan, + targetMatchableAttrs, targetProps); this._parseAssignmentEvent( bindParts[IDENT_KW_IDX], value, srcSpan, attr.valueSpan || srcSpan, targetMatchableAttrs, boundEvents); } else if (bindParts[KW_AT_IDX]) { this._bindingParser.parseLiteralAttr( - name, value, srcSpan, absoluteOffset, targetMatchableAttrs, targetProps); + name, value, srcSpan, absoluteOffset, attr.valueSpan, targetMatchableAttrs, + targetProps); } else if (bindParts[IDENT_BANANA_BOX_IDX]) { 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); this._parseAssignmentEvent( 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]) { this._bindingParser.parsePropertyBinding( - bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, absoluteOffset, + bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, absoluteOffset, attr.valueSpan, targetMatchableAttrs, targetProps); } else if (bindParts[IDENT_EVENT_IDX]) { @@ -478,12 +479,12 @@ class TemplateParseVisitor implements html.Visitor { } } else { hasBinding = this._bindingParser.parsePropertyInterpolation( - name, value, srcSpan, targetMatchableAttrs, targetProps); + name, value, srcSpan, attr.valueSpan, targetMatchableAttrs, targetProps); } if (!hasBinding) { 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))); diff --git a/packages/compiler/test/render3/r3_ast_spans_spec.ts b/packages/compiler/test/render3/r3_ast_spans_spec.ts new file mode 100644 index 0000000000..0afa07a20e --- /dev/null +++ b/packages/compiler/test/render3/r3_ast_spans_spec.ts @@ -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 { + 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 ``; + } + 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('
').toEqual([ + ['Element', '0:17', '0:11', '11:17'], + ['TextAttribute', '5:10', '8:9'], + ]); + }); + + it('is correct for elements with attributes without value', () => { + expectFromHtml('
').toEqual([ + ['Element', '0:13', '0:7', '7:13'], + ['TextAttribute', '5:6', ''], + ]); + }); + }); + + 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('
').toEqual([ + ['Element', '0:26', '0:20', '20:26'], + ['BoundAttribute', '5:19', '17:18'], + ]); + }); + + it('is correct for bound properties without value', () => { + expectFromHtml('
').toEqual([ + ['Element', '0:22', '0:16', '16:22'], + ['BoundAttribute', '5:15', ''], + ]); + }); + + it('is correct for bound properties via bind- ', () => { + expectFromHtml('
').toEqual([ + ['Element', '0:25', '0:19', '19:25'], + ['BoundAttribute', '5:18', '16:17'], + ]); + }); + + it('is correct for bound properties via {{...}}', () => { + expectFromHtml('
').toEqual([ + ['Element', '0:24', '0:18', '18:24'], + ['BoundAttribute', '5:17', '11:16'], + ]); + }); + }); + + describe('templates', () => { + it('is correct for * directives', () => { + expectFromHtml('
').toEqual([ + ['Template', '0:11', '0:11', '11:17'], + ['TextAttribute', '5:10', ''], + ['Element', '0:17', '0:11', '11:17'], + ]); + }); + + it('is correct for ', () => { + expectFromHtml('').toEqual([ + ['Template', '0:13', '0:13', '13:27'], + ]); + }); + + it('is correct for reference via #...', () => { + expectFromHtml('').toEqual([ + ['Template', '0:16', '0:16', '16:30'], + ['Reference', '13:15', ''], + ]); + }); + + it('is correct for reference with name', () => { + expectFromHtml('').toEqual([ + ['Template', '0:20', '0:20', '20:34'], + ['Reference', '13:19', '17:18'], + ]); + }); + + it('is correct for reference via ref-...', () => { + expectFromHtml('').toEqual([ + ['Template', '0:19', '0:19', '19:33'], + ['Reference', '13:18', ''], + ]); + }); + + it('is correct for variables via let-...', () => { + expectFromHtml('').toEqual([ + ['Template', '0:23', '0:23', '23:37'], + ['Variable', '13:22', '20:21'], + ]); + }); + + it('is correct for attributes', () => { + expectFromHtml('').toEqual([ + ['Template', '0:21', '0:21', '21:35'], + ['TextAttribute', '13:20', '17:19'], + ]); + }); + + it('is correct for bound attributes', () => { + expectFromHtml('').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('
').toEqual([ + ['Template', '0:28', '0:28', '28:34'], + ['BoundAttribute', '5:27', ''], + ['BoundAttribute', '5:27', ''], + ['Element', '0:34', '0:28', '28:34'], + ]); + }); + + it('is correct for variables via let ...', () => { + expectFromHtml('
').toEqual([ + ['Template', '0:21', '0:21', '21:27'], + ['TextAttribute', '5:20', ''], + ['Variable', '5:20', ''], + ['Element', '0:27', '0:21', '21:27'], + ]); + }); + + it('is correct for variables via as ...', () => { + expectFromHtml('
').toEqual([ + ['Template', '0:27', '0:27', '27:33'], + ['BoundAttribute', '5:26', ''], + ['Variable', '5:26', ''], + ['Element', '0:33', '0:27', '27:33'], + ]); + }); + }); + + describe('events', () => { + it('is correct for event names case sensitive', () => { + expectFromHtml('
').toEqual([ + ['Element', '0:27', '0:21', '21:27'], + ['BoundEvent', '5:20', '18:19'], + ]); + }); + + it('is correct for bound events via on-', () => { + expectFromHtml('
').toEqual([ + ['Element', '0:24', '0:18', '18:24'], + ['BoundEvent', '5:17', '15:16'], + ]); + }); + + it('is correct for bound events and properties via [(...)]', () => { + expectFromHtml('
').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('
').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('
').toEqual([ + ['Element', '0:14', '0:8', '8:14'], + ['Reference', '5:7', ''], + ]); + }); + + it('is correct for references with name', () => { + expectFromHtml('
').toEqual([ + ['Element', '0:18', '0:12', '12:18'], + ['Reference', '5:11', '9:10'], + ]); + }); + + it('is correct for references via ref-', () => { + expectFromHtml('
').toEqual([ + ['Element', '0:17', '0:11', '11:17'], + ['Reference', '5:10', ''], + ]); + }); + }); +}); diff --git a/packages/compiler/test/template_parser/template_parser_spec.ts b/packages/compiler/test/template_parser/template_parser_spec.ts index c13d2f1dfe..eca9eeac33 100644 --- a/packages/compiler/test/template_parser/template_parser_spec.ts +++ b/packages/compiler/test/template_parser/template_parser_spec.ts @@ -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', () => { expect(() => parse('
', [])).toThrowError(`Template parse errors: -Parser Error: Unexpected token 'b' at column 3 in [a b] in TestComp@0:5 ("
][prop]="a b">
"): TestComp@0:5`); +Parser Error: Unexpected token 'b' at column 3 in [a b] in TestComp@0:13 ("
"): TestComp@0:13`); }); it('should not throw on invalid property names if the property is used by a directive',