diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts
index 7e8c693240..696b425bbc 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {AST, ASTWithSource, BindingPipe, MethodCall, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
+import {AST, ASTWithSource, BindingPipe, MethodCall, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../file_system';
@@ -482,8 +482,20 @@ export class SymbolBuilder {
expression.nameSpan :
expression.sourceSpan;
- let node = findFirstMatchingNode(
- this.typeCheckBlock, {withSpan, filter: (n: ts.Node): n is ts.Node => true});
+ let node: ts.Node|null = null;
+
+ // Property reads in templates usually map to a `PropertyAccessExpression`
+ // (e.g. `ctx.foo`) so try looking for one first.
+ if (expression instanceof PropertyRead) {
+ node = findFirstMatchingNode(
+ this.typeCheckBlock, {withSpan, filter: ts.isPropertyAccessExpression});
+ }
+
+ // Otherwise fall back to searching for any AST node.
+ if (node === null) {
+ node = findFirstMatchingNode(this.typeCheckBlock, {withSpan, filter: anyNodeFilter});
+ }
+
if (node === null) {
return null;
}
@@ -560,3 +572,8 @@ export class SymbolBuilder {
}
}
}
+
+/** Filter predicate function that matches any AST node. */
+function anyNodeFilter(n: ts.Node): n is ts.Node {
+ return true;
+}
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts
index 842cfc5b05..42914425a5 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts
@@ -462,6 +462,50 @@ class TestComponent {
`TestComponent.html(4, 18): Property 'heihgt' does not exist on type 'TestComponent'. Did you mean 'height'?`,
]);
});
+
+ it('works for shorthand property declarations', () => {
+ const messages = diagnose(
+ `
`, `
+ class Dir {
+ input: {a: string, b: number};
+ }
+ class TestComponent {
+ a: number;
+ }`,
+ [{
+ type: 'directive',
+ name: 'Dir',
+ selector: '[dir]',
+ exportAs: ['dir'],
+ inputs: {input: 'input'},
+ }]);
+
+ expect(messages).toEqual(
+ [`TestComponent.html(1, 20): Type 'number' is not assignable to type 'string'.`]);
+ });
+
+ it('works for shorthand property declarations referring to template variables', () => {
+ const messages = diagnose(
+ `
+
+
+ `,
+ `
+ class Dir {
+ input: {span: string, b: number};
+ }
+ class TestComponent {}`,
+ [{
+ type: 'directive',
+ name: 'Dir',
+ selector: '[dir]',
+ exportAs: ['dir'],
+ inputs: {input: 'input'},
+ }]);
+
+ expect(messages).toEqual(
+ [`TestComponent.html(3, 30): Type 'HTMLElement' is not assignable to type 'string'.`]);
+ });
});
describe('method call spans', () => {
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/span_comments_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/span_comments_spec.ts
index 0c153b47f7..3507d8952c 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/test/span_comments_spec.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/test/span_comments_spec.ts
@@ -42,6 +42,15 @@ describe('type check blocks diagnostics', () => {
'(ctx).m /*3,4*/({ "foo": ((ctx).a /*11,12*/) /*11,12*/, "bar": ((ctx).b /*19,20*/) /*19,20*/ } /*5,21*/) /*3,22*/');
});
+ it('should annotate literal map expressions with shorthand declarations', () => {
+ // The additional method call is present to avoid that the object literal is emitted as
+ // statement, which would wrap it into parenthesis that clutter the expected output.
+ const TEMPLATE = '{{ m({a, b}) }}';
+ expect(tcbWithSpans(TEMPLATE))
+ .toContain(
+ '((ctx).m /*3,4*/({ "a": ((ctx).a /*6,7*/) /*6,7*/, "b": ((ctx).b /*9,10*/) /*9,10*/ } /*5,11*/) /*3,12*/)');
+ });
+
it('should annotate literal array expressions', () => {
const TEMPLATE = '{{ [a, b] }}';
expect(tcbWithSpans(TEMPLATE))
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts
index 290da7d875..8c133e94d1 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts
@@ -668,12 +668,16 @@ runInEachFileSystem(() => {
const fileName = absoluteFrom('/main.ts');
const templateString = `
{{ [1, 2, 3] }}
- {{ { hello: "world" } }}`;
+ {{ { hello: "world" } }}
+ {{ { foo } }}`;
const testValues = setup([
{
fileName,
templates: {'Cmp': templateString},
- source: `export class Cmp {}`,
+ source: `
+ type Foo {name: string;}
+ export class Cmp {foo: Foo;}
+ `,
},
]);
templateTypeChecker = testValues.templateTypeChecker;
@@ -701,6 +705,15 @@ runInEachFileSystem(() => {
expect(program.getTypeChecker().typeToString(symbol.tsType))
.toEqual('{ hello: string; }');
});
+
+ it('literal map shorthand property', () => {
+ const shorthandProp =
+ (interpolation.expressions[2] as LiteralMap).values[0] as PropertyRead;
+ const symbol = templateTypeChecker.getSymbolOfNode(shorthandProp, cmp)!;
+ assertExpressionSymbol(symbol);
+ expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('foo');
+ expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('Foo');
+ });
});
describe('pipes', () => {
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/GOLDEN_PARTIAL.js
index 8643d5d3a3..31bd342a11 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/GOLDEN_PARTIAL.js
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/GOLDEN_PARTIAL.js
@@ -870,3 +870,54 @@ export declare class MyModule {
static ɵinj: i0.ɵɵInjectorDeclaration;
}
+/****************************************************************************************************
+ * PARTIAL FILE: shorthand_property_declaration.js
+ ****************************************************************************************************/
+import { Component, NgModule } from '@angular/core';
+import * as i0 from "@angular/core";
+export class MyComponent {
+ constructor() {
+ this.a = 1;
+ this.c = 3;
+ }
+ _handleClick(_value) { }
+}
+MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
+MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, selector: "ng-component", ngImport: i0, template: `
+
+ `, isInline: true });
+i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
+ type: Component,
+ args: [{
+ template: `
+
+ `
+ }]
+ }] });
+export class MyModule {
+}
+MyModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
+MyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, declarations: [MyComponent] });
+MyModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule });
+i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, decorators: [{
+ type: NgModule,
+ args: [{ declarations: [MyComponent] }]
+ }] });
+
+/****************************************************************************************************
+ * PARTIAL FILE: shorthand_property_declaration.d.ts
+ ****************************************************************************************************/
+import * as i0 from "@angular/core";
+export declare class MyComponent {
+ a: number;
+ c: number;
+ _handleClick(_value: any): void;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵcmp: i0.ɵɵComponentDeclaration;
+}
+export declare class MyModule {
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵmod: i0.ɵɵNgModuleDeclaration;
+ static ɵinj: i0.ɵɵInjectorDeclaration;
+}
+
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/TEST_CASES.json
index a8ee6599f7..602379b99f 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/TEST_CASES.json
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/TEST_CASES.json
@@ -269,6 +269,23 @@
"failureMessage": "Incorrect template"
}
]
+ },
+ {
+ "description": "should handle shorthand property declarations in templates",
+ "inputFiles": [
+ "shorthand_property_declaration.ts"
+ ],
+ "expectations": [
+ {
+ "files": [
+ {
+ "expected": "shorthand_property_declaration_template.js",
+ "generated": "shorthand_property_declaration.js"
+ }
+ ],
+ "failureMessage": "Incorrect template"
+ }
+ ]
}
]
}
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/shorthand_property_declaration.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/shorthand_property_declaration.ts
new file mode 100644
index 0000000000..e2a673de48
--- /dev/null
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/shorthand_property_declaration.ts
@@ -0,0 +1,16 @@
+import {Component, NgModule} from '@angular/core';
+
+@Component({
+ template: `
+
+ `
+})
+export class MyComponent {
+ a = 1;
+ c = 3;
+ _handleClick(_value: any) {}
+}
+
+@NgModule({declarations: [MyComponent]})
+export class MyModule {
+}
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/shorthand_property_declaration_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/shorthand_property_declaration_template.js
new file mode 100644
index 0000000000..03de9b391b
--- /dev/null
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_template/shorthand_property_declaration_template.js
@@ -0,0 +1,13 @@
+template: function MyComponent_Template(rf, ctx) {
+ if (rf & 1) {
+ …
+ i0.ɵɵlistener("click", function MyComponent_Template_div_click_0_listener() {
+ return ctx._handleClick({
+ a: ctx.a,
+ b: 2,
+ c: ctx.c
+ });
+ });
+ …
+ }
+}
diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts
index bf25bf46dd..66e5bc2c06 100644
--- a/packages/compiler/src/expression_parser/parser.ts
+++ b/packages/compiler/src/expression_parser/parser.ts
@@ -929,11 +929,23 @@ export class _ParseAST {
if (!this.consumeOptionalCharacter(chars.$RBRACE)) {
this.rbracesExpected++;
do {
+ const keyStart = this.inputIndex;
const quoted = this.next.isString();
const key = this.expectIdentifierOrKeywordOrString();
keys.push({key, quoted});
- this.expectCharacter(chars.$COLON);
- values.push(this.parsePipe());
+
+ // Properties with quoted keys can't use the shorthand syntax.
+ if (quoted) {
+ this.expectCharacter(chars.$COLON);
+ values.push(this.parsePipe());
+ } else if (this.consumeOptionalCharacter(chars.$COLON)) {
+ values.push(this.parsePipe());
+ } else {
+ const span = this.span(keyStart);
+ const sourceSpan = this.sourceSpan(keyStart);
+ values.push(new PropertyRead(
+ span, sourceSpan, sourceSpan, new ImplicitReceiver(span, sourceSpan), key));
+ }
} while (this.consumeOptionalCharacter(chars.$COMMA));
this.rbracesExpected--;
this.expectCharacter(chars.$RBRACE);
diff --git a/packages/compiler/test/expression_parser/parser_spec.ts b/packages/compiler/test/expression_parser/parser_spec.ts
index 5d9be92115..444995483b 100644
--- a/packages/compiler/test/expression_parser/parser_spec.ts
+++ b/packages/compiler/test/expression_parser/parser_spec.ts
@@ -122,6 +122,23 @@ describe('parser', () => {
expectActionError('{1234:0}', 'expected identifier, keyword, or string');
expectActionError('{#myField:0}', 'expected identifier, keyword or string');
});
+
+ it('should parse property shorthand declarations', () => {
+ checkAction('{a, b, c}', '{a: a, b: b, c: c}');
+ checkAction('{a: 1, b}', '{a: 1, b: b}');
+ checkAction('{a, b: 1}', '{a: a, b: 1}');
+ checkAction('{a: 1, b, c: 2}', '{a: 1, b: b, c: 2}');
+ });
+
+ it('should not allow property shorthand declaration on quoted properties', () => {
+ expectActionError('{"a-b"}', 'expected : at column 7');
+ });
+
+ it('should not infer invalid identifiers as shorthand property declarations', () => {
+ expectActionError('{a.b}', 'expected } at column 3');
+ expectActionError('{a["b"]}', 'expected } at column 3');
+ expectActionError('{1234}', ' expected identifier, keyword, or string at column 2');
+ });
});
describe('member access', () => {
diff --git a/packages/compiler/test/render3/r3_ast_absolute_span_spec.ts b/packages/compiler/test/render3/r3_ast_absolute_span_spec.ts
index bb9082b245..9d89aaeb35 100644
--- a/packages/compiler/test/render3/r3_ast_absolute_span_spec.ts
+++ b/packages/compiler/test/render3/r3_ast_absolute_span_spec.ts
@@ -360,4 +360,15 @@ describe('expression AST absolute source spans', () => {
expect(spans).toContain(['nestedPlaceholder', new AbsoluteSourceSpan(89, 106)]);
});
});
+
+ describe('object literal', () => {
+ it('is correct for object literals with shorthand property declarations', () => {
+ const spans =
+ humanizeExpressionSource(parse('').nodes);
+
+ expect(spans).toContain(['{a: 1, b: b, c: 3, foo: foo}', new AbsoluteSourceSpan(19, 39)]);
+ expect(spans).toContain(['b', new AbsoluteSourceSpan(26, 27)]);
+ expect(spans).toContain(['foo', new AbsoluteSourceSpan(35, 38)]);
+ });
+ });
});
diff --git a/packages/core/test/acceptance/integration_spec.ts b/packages/core/test/acceptance/integration_spec.ts
index 9ffb95cdd6..ce00467707 100644
--- a/packages/core/test/acceptance/integration_spec.ts
+++ b/packages/core/test/acceptance/integration_spec.ts
@@ -2041,6 +2041,26 @@ describe('acceptance integration tests', () => {
expect(fixture.nativeElement.innerHTML).toContain('Hello');
});
+ it('should handle shorthand property declarations in templates', () => {
+ @Directive({selector: '[my-dir]'})
+ class Dir {
+ @Input('my-dir') value: any;
+ }
+
+ @Component({template: ``})
+ class App {
+ @ViewChild(Dir) directive!: Dir;
+ a = 1;
+ someProp = 3;
+ }
+
+ TestBed.configureTestingModule({declarations: [App, Dir]});
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.directive.value).toEqual({a: 1, b: 2, someProp: 3});
+ });
+
describe('tView.firstUpdatePass', () => {
function isFirstUpdatePass() {
const lView = getLView();
diff --git a/packages/language-service/ivy/test/legacy/definitions_spec.ts b/packages/language-service/ivy/test/legacy/definitions_spec.ts
index f5fc2b40ce..b7d7efbd10 100644
--- a/packages/language-service/ivy/test/legacy/definitions_spec.ts
+++ b/packages/language-service/ivy/test/legacy/definitions_spec.ts
@@ -458,6 +458,32 @@ describe('definitions', () => {
const definitionAndBoundSpan = ngLS.getDefinitionAndBoundSpan(APP_COMPONENT, position);
expect(definitionAndBoundSpan).toBeUndefined();
});
+
+ it('should work for object literals with shorthand declarations in an action', () => {
+ const definitions = getDefinitionsAndAssertBoundSpan({
+ templateOverride: ``,
+ expectedSpanText: 'name',
+ });
+ expect(definitions!.length).toEqual(1);
+
+ const [def] = definitions;
+ expect(def.textSpan).toEqual('name');
+ expect(def.fileName).toContain('/app/app.component.ts');
+ expect(def.contextSpan).toContain(`name = 'Frodo';`);
+ });
+
+ it('should work for object literals with shorthand declarations in a data binding', () => {
+ const definitions = getDefinitionsAndAssertBoundSpan({
+ templateOverride: `{{ {na¦me} }}`,
+ expectedSpanText: 'name',
+ });
+ expect(definitions!.length).toEqual(1);
+
+ const [def] = definitions;
+ expect(def.textSpan).toEqual('name');
+ expect(def.fileName).toContain('/app/app.component.ts');
+ expect(def.contextSpan).toContain(`name = 'Frodo';`);
+ });
});
describe('external resources', () => {
diff --git a/packages/language-service/ivy/test/legacy/template_target_spec.ts b/packages/language-service/ivy/test/legacy/template_target_spec.ts
index b33535ccad..91d53adc73 100644
--- a/packages/language-service/ivy/test/legacy/template_target_spec.ts
+++ b/packages/language-service/ivy/test/legacy/template_target_spec.ts
@@ -622,6 +622,38 @@ describe('getTargetAtPosition for expression AST', () => {
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.Conditional);
});
+
+ describe('object literal shorthand', () => {
+ it('should locate on literal with one shorthand property', () => {
+ const {errors, nodes, position} = parse(`{{ {va¦l1} }}`);
+ expect(errors).toBe(null);
+ const {context} = getTargetAtPosition(nodes, position)!;
+ expect(context.kind).toBe(TargetNodeKind.RawExpression);
+ const {node} = context as SingleNodeTarget;
+ expect(node).toBeInstanceOf(e.PropertyRead);
+ expect((node as e.PropertyRead).name).toBe('val1');
+ });
+
+ it('should locate on literal with multiple shorthand properties', () => {
+ const {errors, nodes, position} = parse(`{{ {val1, va¦l2} }}`);
+ expect(errors).toBe(null);
+ const {context} = getTargetAtPosition(nodes, position)!;
+ expect(context.kind).toBe(TargetNodeKind.RawExpression);
+ const {node} = context as SingleNodeTarget;
+ expect(node).toBeInstanceOf(e.PropertyRead);
+ expect((node as e.PropertyRead).name).toBe('val2');
+ });
+
+ it('should locale on property with mixed shorthand and regular properties', () => {
+ const {errors, nodes, position} = parse(`{{ {val1: 'val1', va¦l2} }}`);
+ expect(errors).toBe(null);
+ const {context} = getTargetAtPosition(nodes, position)!;
+ expect(context.kind).toBe(TargetNodeKind.RawExpression);
+ const {node} = context as SingleNodeTarget;
+ expect(node).toBeInstanceOf(e.PropertyRead);
+ expect((node as e.PropertyRead).name).toBe('val2');
+ });
+ });
});
describe('findNodeAtPosition for microsyntax expression', () => {
diff --git a/packages/language-service/ivy/test/quick_info_spec.ts b/packages/language-service/ivy/test/quick_info_spec.ts
index a23b9dd350..ab18e13e12 100644
--- a/packages/language-service/ivy/test/quick_info_spec.ts
+++ b/packages/language-service/ivy/test/quick_info_spec.ts
@@ -500,6 +500,48 @@ describe('quick info', () => {
expect(documentation).toBe('This is the title of the `AppCmp` Component.');
});
});
+
+ it('should work for object literal with shorthand property declarations', () => {
+ initMockFileSystem('Native');
+ env = LanguageServiceTestEnv.setup();
+ project = env.addProject(
+ 'test', {
+ 'app.ts': `
+ import {Component, NgModule} from '@angular/core';
+ import {CommonModule} from '@angular/common';
+
+ @Component({
+ selector: 'some-cmp',
+ templateUrl: './app.html',
+ })
+ export class SomeCmp {
+ val1 = 'one';
+ val2 = 2;
+
+ doSomething(obj: {val1: string, val2: number}) {}
+ }
+
+ @NgModule({
+ declarations: [SomeCmp],
+ imports: [CommonModule],
+ })
+ export class AppModule{
+ }
+ `,
+ 'app.html': `{{doSomething({val1, val2})}}`,
+ },
+ {strictTemplates: true});
+ env.expectNoSourceDiagnostics();
+ project.expectNoSourceDiagnostics();
+
+ const template = project.openFile('app.html');
+ template.moveCursorToText('val¦1');
+ const quickInfo = template.getQuickInfoAtPosition();
+ expect(toText(quickInfo!.displayParts)).toEqual('(property) SomeCmp.val1: string');
+ template.moveCursorToText('val¦2');
+ const quickInfo2 = template.getQuickInfoAtPosition();
+ expect(toText(quickInfo2!.displayParts)).toEqual('(property) SomeCmp.val2: number');
+ });
});
describe('generics', () => {
diff --git a/packages/language-service/ivy/test/references_and_rename_spec.ts b/packages/language-service/ivy/test/references_and_rename_spec.ts
index a8a994cbf3..c732dec80e 100644
--- a/packages/language-service/ivy/test/references_and_rename_spec.ts
+++ b/packages/language-service/ivy/test/references_and_rename_spec.ts
@@ -235,7 +235,7 @@ describe('find references and rename locations', () => {
beforeEach(() => {
const files = {
- 'app.ts': `
+ 'app.ts': `
import {Component} from '@angular/core';
@Component({template: '' })
@@ -617,7 +617,7 @@ describe('find references and rename locations', () => {
let file: OpenBuffer;
beforeEach(() => {
const files = {
- 'app.ts': `
+ 'app.ts': `
import {Component} from '@angular/core';
@Component({template: '{{iRef}}
'})
@@ -739,6 +739,42 @@ describe('find references and rename locations', () => {
assertTextSpans(renameLocations, ['name']);
});
});
+
+ describe('when cursor is on property read of variable', () => {
+ let file: OpenBuffer;
+ beforeEach(() => {
+ const files = {
+ 'app.ts': `
+ import {Component} from '@angular/core';
+
+ @Component({template: ''})
+ export class AppCmp {
+ name = 'Frodo';
+
+ setHero(hero: {name: string}) {}
+ }`
+ };
+
+ env = LanguageServiceTestEnv.setup();
+ const project = createModuleAndProjectWithDeclarations(env, 'test', files);
+ file = project.openFile('app.ts');
+ file.moveCursorToText('{na¦me}');
+ });
+
+ it('should find references', () => {
+ const refs = getReferencesAtPosition(file)!;
+ expect(refs.length).toBe(2);
+ assertFileNames(refs, ['app.ts']);
+ assertTextSpans(refs, ['name']);
+ });
+
+ it('should find rename locations', () => {
+ const renameLocations = getRenameLocationsAtPosition(file)!;
+ expect(renameLocations.length).toBe(2);
+ assertFileNames(renameLocations, ['app.ts']);
+ assertTextSpans(renameLocations, ['name']);
+ });
+ });
});
describe('pipes', () => {
@@ -1245,7 +1281,7 @@ describe('find references and rename locations', () => {
}`,
'app.ts': `
import {Component} from '@angular/core';
-
+
@Component({template: ''})
export class AppCmp {
title = 'title';
diff --git a/packages/language-service/test/project/app/app.component.ts b/packages/language-service/test/project/app/app.component.ts
index 1bcbbe95eb..67a24e0da1 100644
--- a/packages/language-service/test/project/app/app.component.ts
+++ b/packages/language-service/test/project/app/app.component.ts
@@ -48,7 +48,11 @@ export class AppComponent {
constNames = [{name: 'name'}] as const;
private myField = 'My Field';
strOrNumber: string|number = '';
+ name = 'Frodo';
setTitle(newTitle: string) {
this.title = newTitle;
}
+ setHero(obj: Hero) {
+ this.hero = obj;
+ }
}