diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 4e3ffff828..307970172d 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -435,16 +435,24 @@ export function queriesFromFields( fields: {member: ClassMember, decorators: Decorator[]}[], reflector: ReflectionHost, evaluator: PartialEvaluator): R3QueryMetadata[] { return fields.map(({member, decorators}) => { + const decorator = decorators[0]; + const node = member.node || decorator.node; + // Throw in case of `@Input() @ContentChild('foo') foo: any`, which is not supported in Ivy if (member.decorators !.some(v => v.name === 'Input')) { - throw new Error(`Cannot combine @Input decorators with query decorators`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_COLLISION, node, + 'Cannot combine @Input decorators with query decorators'); } if (decorators.length !== 1) { - throw new Error(`Cannot have multiple query decorators on the same class member`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_COLLISION, node, + 'Cannot have multiple query decorators on the same class member'); } else if (!isPropertyTypeMember(member)) { - throw new Error(`Query decorator must go on a property-type member`); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_UNEXPECTED, node, + 'Query decorator must go on a property-type member'); } - const decorator = decorators[0]; return extractQueryMetadata( decorator.node, decorator.name, decorator.args || [], member.name, reflector, evaluator); }); diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/code.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/code.ts index 08988120fe..ee2ca86614 100644 --- a/packages/compiler-cli/src/ngtsc/diagnostics/src/code.ts +++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/code.ts @@ -14,7 +14,7 @@ export enum ErrorCode { DECORATOR_UNEXPECTED = 1005, /** - * This error code indicates that there are incompatible decorators on a type. + * This error code indicates that there are incompatible decorators on a type or a class field. */ DECORATOR_COLLISION = 1006, diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index bf18bfe0c0..d441544140 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -1985,44 +1985,6 @@ describe('compiler compliance', () => { expectEmit(source, ContentQueryComponentDefinition, 'Invalid ContentQuery declaration'); }); - - it('should throw error if content queries share a property with inputs', () => { - const files = { - app: { - ...directive, - 'content_query.ts': ` - import {Component, ContentChild, Input, NgModule} from '@angular/core'; - - @Component({ - selector: 'content-query-component', - template: \` -
- \` - }) - export class ContentQueryComponent { - @Input() @ContentChild('foo', {static: false}) foo: any; - } - - @Component({ - selector: 'my-app', - template: \` - -
-
- \` - }) - export class MyApp { } - - @NgModule({declarations: [ContentQueryComponent, MyApp]}) - export class MyModule { } - ` - } - }; - - expect(() => compile(files, angularFiles)) - .toThrowError(/Cannot combine @Input decorators with query decorators/); - }); - }); describe('pipes', () => { diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 7eb82aef91..320b68b078 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {ErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics'; import {LazyRoute} from '@angular/compiler-cli/src/ngtsc/routing'; import * as path from 'path'; import * as ts from 'typescript'; @@ -1018,6 +1019,72 @@ describe('ngtsc behavioral tests', () => { expect(trim(errors[0].messageText as string)) .toContain('Directive TestDir has no selector, please add it!'); }); + + it('should throw error if content queries share a property with inputs', () => { + env.tsconfig({}); + env.write('test.ts', ` + import {Component, ContentChild, Input} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: '' + }) + export class TestCmp { + @Input() @ContentChild('foo', {static: false}) foo: any; + } + `); + + const errors = env.driveDiagnostics(); + const {code, messageText} = errors[0]; + expect(code).toBe(ngErrorCode(ErrorCode.DECORATOR_COLLISION)); + expect(trim(messageText as string)) + .toContain('Cannot combine @Input decorators with query decorators'); + }); + + it('should throw error if multiple query decorators are used on the same field', () => { + env.tsconfig({}); + env.write('test.ts', ` + import {Component, ContentChild} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: '...' + }) + export class TestCmp { + @ContentChild('bar', {static: true}) + @ContentChild('foo', {static: false}) + foo: any; + } + `); + + const errors = env.driveDiagnostics(); + const {code, messageText} = errors[0]; + expect(code).toBe(ngErrorCode(ErrorCode.DECORATOR_COLLISION)); + expect(trim(messageText as string)) + .toContain('Cannot have multiple query decorators on the same class member'); + }); + + it('should throw error if query decorators are used on non property-type member', () => { + env.tsconfig({}); + env.write('test.ts', ` + import {Component, ContentChild} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: '...' + }) + export class TestCmp { + @ContentChild('foo', {static: false}) + private someFn() {} + } + `); + + const errors = env.driveDiagnostics(); + const {code, messageText} = errors[0]; + expect(code).toBe(ngErrorCode(ErrorCode.DECORATOR_UNEXPECTED)); + expect(trim(messageText as string)) + .toContain('Query decorator must go on a property-type member'); + }); }); describe('multiple decorators on classes', () => {