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', () => {