diff --git a/packages/compiler/src/template_parser/template_parser.ts b/packages/compiler/src/template_parser/template_parser.ts index e17cf35ea6..85c35a6f98 100644 --- a/packages/compiler/src/template_parser/template_parser.ts +++ b/packages/compiler/src/template_parser/template_parser.ts @@ -305,9 +305,10 @@ class TemplateParseVisitor implements html.Visitor { } hasInlineTemplates = true; const parsedVariables: ParsedVariable[] = []; + const absoluteOffset = (attr.valueSpan || attr.sourceSpan).start.offset; this._bindingParser.parseInlineTemplateBinding( - templateKey !, templateValue !, attr.sourceSpan, attr.sourceSpan.start.offset, - templateMatchableAttrs, templateElementOrDirectiveProps, parsedVariables); + templateKey !, templateValue !, attr.sourceSpan, absoluteOffset, templateMatchableAttrs, + templateElementOrDirectiveProps, parsedVariables); templateElementVars.push(...parsedVariables.map(v => t.VariableAst.fromParsedVariable(v))); } diff --git a/packages/compiler/test/template_parser/template_parser_absolute_span_spec.ts b/packages/compiler/test/template_parser/template_parser_absolute_span_spec.ts new file mode 100644 index 0000000000..4e922720bb --- /dev/null +++ b/packages/compiler/test/template_parser/template_parser_absolute_span_spec.ts @@ -0,0 +1,365 @@ +/** + * @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 {AbsoluteSourceSpan, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, SchemaMetadata} from '@angular/compiler'; +import {TemplateAst} from '@angular/compiler/src/template_parser/template_ast'; +import {TemplateParser} from '@angular/compiler/src/template_parser/template_parser'; +import {inject} from '@angular/core/testing'; + +import {humanizeExpressionSource} from './util/expression'; +import {compileDirectiveMetadataCreate, compileTemplateMetadata, createTypeMeta} from './util/metadata'; + +describe('expression AST absolute source spans', () => { + const fakeTemplate = compileTemplateMetadata({animations: []}); + const fakeComponent = compileDirectiveMetadataCreate({ + isHost: false, + selector: 'app-fake', + template: fakeTemplate, + type: createTypeMeta({reference: {filePath: 'fake-path', name: 'FakeComponent'}}), + isComponent: true + }); + const ngIf = compileDirectiveMetadataCreate({ + selector: '[ngIf]', + template: fakeTemplate, + type: createTypeMeta({reference: {filePath: 'fake-path', name: 'NgIf'}}), + inputs: ['ngIf'] + }).toSummary(); + let parse: ( + template: string, directives?: CompileDirectiveSummary[], pipes?: CompilePipeSummary[], + schemas?: SchemaMetadata[], preserveWhitespaces?: boolean) => TemplateAst[]; + + beforeEach(inject([TemplateParser], (parser: TemplateParser) => { + parse = + (template: string, directives: CompileDirectiveSummary[] = [], + pipes: CompilePipeSummary[] | null = null, schemas: SchemaMetadata[] = [], + preserveWhitespaces = true): TemplateAst[] => { + if (pipes === null) { + pipes = []; + } + return parser + .parse( + fakeComponent, template, directives, pipes, schemas, 'TestComponent', + preserveWhitespaces) + .template; + }; + })); + + it('should provide absolute offsets of an expression in a bound text', () => { + expect(humanizeExpressionSource(parse('
{{foo}}
'))).toContain([ + '{{ foo }}', new AbsoluteSourceSpan(5, 12) + ]); + }); + + it('should provide absolute offsets of an expression in a bound event', () => { + expect(humanizeExpressionSource(parse('
'))).toContain([ + 'foo(); bar();', new AbsoluteSourceSpan(14, 26) + ]); + + expect(humanizeExpressionSource(parse('
'))).toContain([ + 'foo(); bar();', new AbsoluteSourceSpan(15, 27) + ]); + }); + + it('should provide absolute offsets of an expression in a bound attribute', () => { + expect(humanizeExpressionSource(parse(''))) + .toContain(['condition ? true : false', new AbsoluteSourceSpan(19, 43)]); + + expect(humanizeExpressionSource(parse(''))) + .toContain(['condition ? true : false', new AbsoluteSourceSpan(22, 46)]); + }); + + it('should provide absolute offsets of an expression in a template attribute', () => { + const ngTemplate = + compileDirectiveMetadataCreate({ + selector: 'ng-template', + type: createTypeMeta({reference: {filePath: 'fake-path', name: 'OnTemplate'}}) + }).toSummary(); + + expect(humanizeExpressionSource(parse('
', [ngIf, ngTemplate]))) + .toContain(['value', new AbsoluteSourceSpan(12, 17)]); + }); + + describe('binary expression', () => { + it('should provide absolute offsets of a binary expression', () => { + expect(humanizeExpressionSource(parse('
{{1 + 2}}
'))).toContain([ + '1 + 2', new AbsoluteSourceSpan(7, 12) + ]); + }); + + it('should provide absolute offsets of expressions in a binary expression', () => { + expect(humanizeExpressionSource(parse('
{{1 + 2}}
'))) + .toEqual(jasmine.arrayContaining([ + // TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions + // with trailing whitespace in a binary expression. Look into fixing this. + ['1', new AbsoluteSourceSpan(7, 9)], + ['2', new AbsoluteSourceSpan(11, 12)], + ])); + }); + }); + + describe('conditional', () => { + it('should provide absolute offsets of a conditional', () => { + expect(humanizeExpressionSource(parse('
{{bool ? 1 : 0}}
'))).toContain([ + 'bool ? 1 : 0', new AbsoluteSourceSpan(7, 19) + ]); + }); + + it('should provide absolute offsets of expressions in a conditional', () => { + expect(humanizeExpressionSource(parse('
{{bool ? 1 : 0}}
'))) + .toEqual(jasmine.arrayContaining([ + // TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions + // with trailing whitespace in a conditional expression. Look into fixing this. + ['bool', new AbsoluteSourceSpan(7, 12)], + ['1', new AbsoluteSourceSpan(14, 16)], + ['0', new AbsoluteSourceSpan(18, 19)], + ])); + }); + }); + + describe('chain', () => { + it('should provide absolute offsets of a chain', () => { + expect(humanizeExpressionSource(parse('
'))).toContain([ + 'a(); b();', new AbsoluteSourceSpan(14, 23) + ]); + }); + + it('should provide absolute offsets of expressions in a chain', () => { + expect(humanizeExpressionSource(parse('
'))) + .toEqual(jasmine.arrayContaining([ + ['a()', new AbsoluteSourceSpan(14, 17)], + ['b()', new AbsoluteSourceSpan(19, 22)], + ])); + }); + }); + + describe('function call', () => { + it('should provide absolute offsets of a function call', () => { + expect(humanizeExpressionSource(parse('
{{fn()()}}
'))).toContain([ + 'fn()()', new AbsoluteSourceSpan(7, 13) + ]); + }); + + it('should provide absolute offsets of expressions in a function call', () => { + expect(humanizeExpressionSource(parse('
{{fn()(param)}}
'))).toContain([ + 'param', new AbsoluteSourceSpan(12, 17) + ]); + }); + }); + + it('should provide absolute offsets of an implicit receiver', () => { + expect(humanizeExpressionSource(parse('
{{a.b}}
'))).toContain([ + '', new AbsoluteSourceSpan(7, 7) + ]); + }); + + describe('interpolation', () => { + it('should provide absolute offsets of an interpolation', () => { + expect(humanizeExpressionSource(parse('
{{1 + foo.length}}
'))).toContain([ + '{{ 1 + foo.length }}', new AbsoluteSourceSpan(5, 23) + ]); + }); + + it('should provide absolute offsets of expressions in an interpolation', () => { + expect(humanizeExpressionSource(parse('
{{1 + 2}}
'))) + .toEqual(jasmine.arrayContaining([ + // TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions + // with trailing whitespace in a conditional expression. Look into fixing this. + ['1', new AbsoluteSourceSpan(7, 9)], + ['2', new AbsoluteSourceSpan(11, 12)], + ])); + }); + }); + + describe('keyed read', () => { + it('should provide absolute offsets of a keyed read', () => { + expect(humanizeExpressionSource(parse('
{{obj[key]}}
'))).toContain([ + 'obj[key]', new AbsoluteSourceSpan(7, 15) + ]); + }); + + it('should provide absolute offsets of expressions in a keyed read', () => { + expect(humanizeExpressionSource(parse('
{{obj[key]}}
'))).toContain([ + 'key', new AbsoluteSourceSpan(11, 14) + ]); + }); + }); + + describe('keyed write', () => { + it('should provide absolute offsets of a keyed write', () => { + expect(humanizeExpressionSource(parse('
{{obj[key] = 0}}
'))).toContain([ + 'obj[key] = 0', new AbsoluteSourceSpan(7, 19) + ]); + }); + + it('should provide absolute offsets of expressions in a keyed write', () => { + expect(humanizeExpressionSource(parse('
{{obj[key] = 0}}
'))) + .toEqual(jasmine.arrayContaining([ + ['key', new AbsoluteSourceSpan(11, 14)], + ['0', new AbsoluteSourceSpan(18, 19)], + ])); + }); + }); + + it('should provide absolute offsets of a literal primitive', () => { + expect(humanizeExpressionSource(parse('
{{100}}
'))).toContain([ + '100', new AbsoluteSourceSpan(7, 10) + ]); + }); + + describe('literal array', () => { + it('should provide absolute offsets of a literal array', () => { + expect(humanizeExpressionSource(parse('
{{[0, 1, 2]}}
'))).toContain([ + '[0, 1, 2]', new AbsoluteSourceSpan(7, 16) + ]); + }); + + it('should provide absolute offsets of expressions in a literal array', () => { + expect(humanizeExpressionSource(parse('
{{[0, 1, 2]}}
'))) + .toEqual(jasmine.arrayContaining([ + ['0', new AbsoluteSourceSpan(8, 9)], + ['1', new AbsoluteSourceSpan(11, 12)], + ['2', new AbsoluteSourceSpan(14, 15)], + ])); + }); + }); + + describe('literal map', () => { + it('should provide absolute offsets of a literal map', () => { + expect(humanizeExpressionSource(parse('
{{ {a: 0} }}
'))).toContain([ + // TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions + // with trailing whitespace in a literal map. Look into fixing this. + '{a: 0}', new AbsoluteSourceSpan(8, 15) + ]); + }); + + it('should provide absolute offsets of expressions in a literal map', () => { + expect(humanizeExpressionSource(parse('
{{ {a: 0} }}
'))) + .toEqual(jasmine.arrayContaining([ + ['0', new AbsoluteSourceSpan(12, 13)], + ])); + }); + }); + + describe('method call', () => { + it('should provide absolute offsets of a method call', () => { + expect(humanizeExpressionSource(parse('
{{method()}}
'))).toContain([ + 'method()', new AbsoluteSourceSpan(7, 15) + ]); + }); + + it('should provide absolute offsets of expressions in a method call', () => { + expect(humanizeExpressionSource(parse('
{{method(param)}}
'))).toContain([ + 'param', new AbsoluteSourceSpan(14, 19) + ]); + }); + }); + + describe('non-null assert', () => { + it('should provide absolute offsets of a non-null assert', () => { + expect(humanizeExpressionSource(parse('
{{prop!}}
'))).toContain([ + 'prop!', new AbsoluteSourceSpan(7, 12) + ]); + }); + + it('should provide absolute offsets of expressions in a non-null assert', () => { + expect(humanizeExpressionSource(parse('
{{prop!}}
'))).toContain([ + 'prop', new AbsoluteSourceSpan(7, 11) + ]); + }); + }); + + describe('pipe', () => { + const testPipe = new CompilePipeMetadata({ + name: 'test', + type: createTypeMeta({reference: {filePath: 'fake-path', name: 'TestPipe'}}), + pure: false + }).toSummary(); + + it('should provide absolute offsets of a pipe', () => { + expect(humanizeExpressionSource(parse('
{{prop | test}}
', [], [testPipe]))) + .toContain(['(prop | test)', new AbsoluteSourceSpan(7, 18)]); + }); + + it('should provide absolute offsets expressions in a pipe', () => { + expect(humanizeExpressionSource(parse('
{{prop | test}}
', [], [testPipe]))) + .toContain([ + // TODO(ayazhafiz): The expression parser includes an extra whitespace on a expressions + // with trailing whitespace in a pipe. Look into fixing this. + 'prop', new AbsoluteSourceSpan(7, 12) + ]); + }); + }); + + it('should provide absolute offsets of a property read', () => { + expect(humanizeExpressionSource(parse('
{{prop}}
'))).toContain([ + 'prop', new AbsoluteSourceSpan(7, 11) + ]); + }); + + describe('property write', () => { + it('should provide absolute offsets of a property write', () => { + expect(humanizeExpressionSource(parse('
'))).toContain([ + 'prop = 0', new AbsoluteSourceSpan(14, 22) + ]); + }); + + it('should provide absolute offsets of expressions in a property write', () => { + expect(humanizeExpressionSource(parse('
'))).toContain([ + '0', new AbsoluteSourceSpan(21, 22) + ]); + }); + }); + + describe('"not" prefix', () => { + it('should provide absolute offsets of a "not" prefix', () => { + expect(humanizeExpressionSource(parse('
{{!prop}}
'))).toContain([ + '!prop', new AbsoluteSourceSpan(7, 12) + ]); + }); + + it('should provide absolute offsets of expressions in a "not" prefix', () => { + expect(humanizeExpressionSource(parse('
{{!prop}}
'))).toContain([ + 'prop', new AbsoluteSourceSpan(8, 12) + ]); + }); + }); + + describe('safe method call', () => { + it('should provide absolute offsets of a safe method call', () => { + expect(humanizeExpressionSource(parse('
{{prop?.safe()}}
'))).toContain([ + 'prop?.safe()', new AbsoluteSourceSpan(7, 19) + ]); + }); + + it('should provide absolute offsets of expressions in safe method call', () => { + expect(humanizeExpressionSource(parse('
{{prop?.safe()}}
'))).toContain([ + 'prop', new AbsoluteSourceSpan(7, 11) + ]); + }); + }); + + describe('safe property read', () => { + it('should provide absolute offsets of a safe property read', () => { + expect(humanizeExpressionSource(parse('
{{prop?.safe}}
'))).toContain([ + 'prop?.safe', new AbsoluteSourceSpan(7, 17) + ]); + }); + + it('should provide absolute offsets of expressions in safe property read', () => { + expect(humanizeExpressionSource(parse('
{{prop?.safe}}
'))).toContain([ + 'prop', new AbsoluteSourceSpan(7, 11) + ]); + }); + }); + + it('should provide absolute offsets of a quote', () => { + expect(humanizeExpressionSource(parse('
'))).toContain([ + 'a:b', new AbsoluteSourceSpan(25, 28) + ]); + }); +}); diff --git a/packages/compiler/test/template_parser/template_parser_spec.ts b/packages/compiler/test/template_parser/template_parser_spec.ts index 0d805187ec..df188cc0ab 100644 --- a/packages/compiler/test/template_parser/template_parser_spec.ts +++ b/packages/compiler/test/template_parser/template_parser_spec.ts @@ -5,24 +5,24 @@ * 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 {CompileQueryMetadata, CompilerConfig, ProxyClass, StaticSymbol, preserveWhitespacesDefault} from '@angular/compiler'; -import {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata, tokenReference} from '@angular/compiler/src/compile_metadata'; +import {preserveWhitespacesDefault} from '@angular/compiler'; +import {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileTemplateMetadata, CompileTokenMetadata, tokenReference} from '@angular/compiler/src/compile_metadata'; import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry'; import {ElementSchemaRegistry} from '@angular/compiler/src/schema/element_schema_registry'; import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAstType, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler/src/template_parser/template_ast'; import {TemplateParser, splitClasses} from '@angular/compiler/src/template_parser/template_parser'; -import {ChangeDetectionStrategy, ComponentFactory, RendererType2, SchemaMetadata, SecurityContext, ViewEncapsulation} from '@angular/core'; +import {SchemaMetadata, SecurityContext} from '@angular/core'; import {Console} from '@angular/core/src/console'; import {TestBed, inject} from '@angular/core/testing'; import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector'; -import {CompileEntryComponentMetadata, CompileStylesheetMetadata} from '../../src/compile_metadata'; import {Identifiers, createTokenForExternalReference, createTokenForReference} from '../../src/identifiers'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/ml_parser/interpolation_config'; -import {newArray, noUndefined} from '../../src/util'; +import {newArray} from '../../src/util'; import {MockSchemaRegistry} from '../../testing'; import {unparse} from '../expression_parser/utils/unparser'; import {TEST_COMPILER_PROVIDERS} from '../test_bindings'; +import {compileDirectiveMetadataCreate, compileTemplateMetadata, createTypeMeta} from './util/metadata'; const someModuleUrl = 'package:someModule'; @@ -33,88 +33,6 @@ const MOCK_SCHEMA_REGISTRY = [{ ['onEvent'], ['onEvent']), }]; -function createTypeMeta({reference, diDeps}: {reference: any, diDeps?: any[]}): - CompileTypeMetadata { - return {reference: reference, diDeps: diDeps || [], lifecycleHooks: []}; -} - -function compileDirectiveMetadataCreate( - {isHost, type, isComponent, selector, exportAs, changeDetection, inputs, outputs, host, - providers, viewProviders, queries, guards, viewQueries, entryComponents, template, - componentViewType, rendererType}: { - isHost?: boolean, - type?: CompileTypeMetadata, - isComponent?: boolean, - selector?: string | null, - exportAs?: string | null, - changeDetection?: ChangeDetectionStrategy | null, - inputs?: string[], - outputs?: string[], - host?: {[key: string]: string}, - providers?: CompileProviderMetadata[] | null, - viewProviders?: CompileProviderMetadata[] | null, - queries?: CompileQueryMetadata[] | null, - guards?: {[key: string]: any}, - viewQueries?: CompileQueryMetadata[], - entryComponents?: CompileEntryComponentMetadata[], - template?: CompileTemplateMetadata, - componentViewType?: StaticSymbol | ProxyClass | null, - rendererType?: StaticSymbol | RendererType2 | null, - }) { - return CompileDirectiveMetadata.create({ - isHost: !!isHost, - type: noUndefined(type) !, - isComponent: !!isComponent, - selector: noUndefined(selector), - exportAs: noUndefined(exportAs), - changeDetection: null, - inputs: inputs || [], - outputs: outputs || [], - host: host || {}, - providers: providers || [], - viewProviders: viewProviders || [], - queries: queries || [], - guards: guards || {}, - viewQueries: viewQueries || [], - entryComponents: entryComponents || [], - template: noUndefined(template) !, - componentViewType: noUndefined(componentViewType), - rendererType: noUndefined(rendererType), - componentFactory: null, - }); -} - -function compileTemplateMetadata({encapsulation, template, templateUrl, styles, styleUrls, - externalStylesheets, animations, ngContentSelectors, - interpolation, isInline, preserveWhitespaces}: { - encapsulation?: ViewEncapsulation | null, - template?: string | null, - templateUrl?: string | null, - styles?: string[], - styleUrls?: string[], - externalStylesheets?: CompileStylesheetMetadata[], - ngContentSelectors?: string[], - animations?: any[], - interpolation?: [string, string] | null, - isInline?: boolean, - preserveWhitespaces?: boolean | null, -}): CompileTemplateMetadata { - return new CompileTemplateMetadata({ - encapsulation: noUndefined(encapsulation), - template: noUndefined(template), - templateUrl: noUndefined(templateUrl), - htmlAst: null, - styles: styles || [], - styleUrls: styleUrls || [], - externalStylesheets: externalStylesheets || [], - animations: animations || [], - ngContentSelectors: ngContentSelectors || [], - interpolation: noUndefined(interpolation), - isInline: !!isInline, - preserveWhitespaces: preserveWhitespacesDefault(noUndefined(preserveWhitespaces)), - }); -} - function humanizeTplAst( templateAsts: TemplateAst[], interpolationConfig?: InterpolationConfig): any[] { @@ -1136,9 +1054,10 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("[0]>) { + return CompileDirectiveMetadata.create({ + isHost: !!isHost, + type: noUndefined(type) !, + isComponent: !!isComponent, + selector: noUndefined(selector), + exportAs: noUndefined(exportAs), + changeDetection: null, + inputs: inputs || [], + outputs: outputs || [], + host: host || {}, + providers: providers || [], + viewProviders: viewProviders || [], + queries: queries || [], + guards: guards || {}, + viewQueries: viewQueries || [], + entryComponents: entryComponents || [], + template: noUndefined(template) !, + componentViewType: noUndefined(componentViewType), + rendererType: noUndefined(rendererType), + componentFactory: null, + }); +} + +export function compileTemplateMetadata( + {encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets, animations, + ngContentSelectors, interpolation, isInline, + preserveWhitespaces}: Partial): CompileTemplateMetadata { + return new CompileTemplateMetadata({ + encapsulation: noUndefined(encapsulation), + template: noUndefined(template), + templateUrl: noUndefined(templateUrl), + htmlAst: null, + styles: styles || [], + styleUrls: styleUrls || [], + externalStylesheets: externalStylesheets || [], + animations: animations || [], + ngContentSelectors: ngContentSelectors || [], + interpolation: noUndefined(interpolation), + isInline: !!isInline, + preserveWhitespaces: preserveWhitespacesDefault(noUndefined(preserveWhitespaces)), + }); +}