diff --git a/goldens/public-api/compiler-cli/error_code.api.md b/goldens/public-api/compiler-cli/error_code.api.md index b4005fd369a..a02e8c70045 100644 --- a/goldens/public-api/compiler-cli/error_code.api.md +++ b/goldens/public-api/compiler-cli/error_code.api.md @@ -49,6 +49,7 @@ export enum ErrorCode { DIRECTIVE_INHERITS_UNDECORATED_CTOR = 2006, // (undocumented) DIRECTIVE_MISSING_SELECTOR = 2004, + DUPLICATE_BINDING_NAME = 1054, // (undocumented) DUPLICATE_DECORATED_PROPERTIES = 1012, DUPLICATE_VARIABLE_DECLARATION = 8006, diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts index 9bde1cfcb05..dcb3b72180b 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts @@ -1346,6 +1346,7 @@ function parseInputFields( emitDeclarationOnly: boolean, ): Record { const inputs = {} as Record; + const bindings = new Map(); for (const member of members) { const classPropertyName = member.name; @@ -1364,6 +1365,18 @@ function parseInputFields( continue; } + const bindingPropertyName = inputMapping.bindingPropertyName; + if (bindings.has(bindingPropertyName)) { + const firstMember = bindings.get(bindingPropertyName)!; + throw new FatalDiagnosticError( + ErrorCode.DUPLICATE_BINDING_NAME, + member.node ?? clazz, + `Input '${bindingPropertyName}' is bound to both '${firstMember.name}' and '${member.name}'.`, + [makeRelatedInformation(firstMember.node ?? clazz, `The first binding is declared here.`)], + ); + } + bindings.set(bindingPropertyName, member); + if (member.isStatic) { throw new FatalDiagnosticError( ErrorCode.INCORRECTLY_DECLARED_ON_STATIC_MEMBER, @@ -1742,6 +1755,7 @@ function parseOutputFields( outputsFromMeta: Record, ): Record { const outputs = {} as Record; + const bindings = new Map(); for (const member of members) { const decoratorOutput = tryParseDecoratorOutput(member, evaluator, isCore); @@ -1786,6 +1800,17 @@ function parseOutputFields( continue; } + if (bindings.has(bindingPropertyName)) { + const firstMember = bindings.get(bindingPropertyName)!; + throw new FatalDiagnosticError( + ErrorCode.DUPLICATE_BINDING_NAME, + member.node ?? clazz, + `Output '${bindingPropertyName}' is bound to both '${firstMember.name}' and '${member.name}'.`, + [makeRelatedInformation(firstMember.node ?? clazz, `The first binding is declared here.`)], + ); + } + bindings.set(bindingPropertyName, member); + // Validate that initializer-based outputs are not accidentally declared // in the `outputs` class metadata. if ( diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts index 9313b32cfb3..5fe8ebcc436 100644 --- a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts +++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts @@ -52,6 +52,11 @@ export enum ErrorCode { */ INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY = 1053, + /** + * Raised whenever there are duplicate binding property names for outputs, inputs & models. + */ + DUPLICATE_BINDING_NAME = 1054, + /** * An Angular feature, like inputs, outputs or queries is incorrectly * declared on a static member. diff --git a/packages/compiler-cli/test/compliance/test_cases/output_function/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/output_function/GOLDEN_PARTIAL.js index 66a29f3f602..0450a1259cc 100644 --- a/packages/compiler-cli/test/compliance/test_cases/output_function/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/output_function/GOLDEN_PARTIAL.js @@ -84,7 +84,7 @@ export class TestDir { clickDecorator2 = new EventEmitter(); _blaDecorator = new EventEmitter(); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, deps: [], target: i0.ɵɵFactoryTarget.Directive }); - static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestDir, isStandalone: true, outputs: { click1: "click1", click2: "click2", click3: "click3", _bla: "decoratorPublicName", _bla2: "decoratorPublicName2", clickDecorator1: "clickDecorator1", clickDecorator2: "clickDecorator2", _blaDecorator: "decoratorPublicName" }, ngImport: i0 }); + static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestDir, isStandalone: true, outputs: { click1: "click1", click2: "click2", click3: "click3", _bla: "decoratorPublicName", _bla2: "decoratorPublicName2", clickDecorator1: "clickDecorator1", clickDecorator2: "clickDecorator2", _blaDecorator: "decoratorPublicName3" }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, decorators: [{ type: Directive @@ -94,7 +94,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE type: Output }], _blaDecorator: [{ type: Output, - args: ['decoratorPublicName'] + args: ['decoratorPublicName3'] }] } }); /**************************************************************************************************** @@ -112,6 +112,6 @@ export declare class TestDir { clickDecorator2: EventEmitter; _blaDecorator: EventEmitter; static ɵfac: i0.ɵɵFactoryDeclaration; - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; } diff --git a/packages/compiler-cli/test/compliance/test_cases/output_function/mixed_variants.js b/packages/compiler-cli/test/compliance/test_cases/output_function/mixed_variants.js index 700ea474d1c..28a89120da9 100644 --- a/packages/compiler-cli/test/compliance/test_cases/output_function/mixed_variants.js +++ b/packages/compiler-cli/test/compliance/test_cases/output_function/mixed_variants.js @@ -10,7 +10,7 @@ export class TestDir { _bla2: "decoratorPublicName2", clickDecorator1: "clickDecorator1", clickDecorator2: "clickDecorator2", - _blaDecorator: "decoratorPublicName" + _blaDecorator: "decoratorPublicName3" } … }); diff --git a/packages/compiler-cli/test/compliance/test_cases/output_function/mixed_variants.ts b/packages/compiler-cli/test/compliance/test_cases/output_function/mixed_variants.ts index 58442d675d6..9ec6fe326c7 100644 --- a/packages/compiler-cli/test/compliance/test_cases/output_function/mixed_variants.ts +++ b/packages/compiler-cli/test/compliance/test_cases/output_function/mixed_variants.ts @@ -11,5 +11,5 @@ export class TestDir { @Output() clickDecorator1 = new EventEmitter(); @Output() clickDecorator2 = new EventEmitter(); - @Output('decoratorPublicName') _blaDecorator = new EventEmitter(); + @Output('decoratorPublicName3') _blaDecorator = new EventEmitter(); } diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index fdb38a2db8a..7e812496311 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -155,7 +155,8 @@ runInEachFileSystem(() => { expect(diags[0].code).toBeGreaterThan(0); }); - it('should produce diagnostics when mapping to multiple fields and bound types are incorrect', () => { + // This is not supported at runtime + xit('should produce diagnostics when mapping to multiple fields and bound types are incorrect', () => { env.tsconfig({ fullTemplateTypeCheck: true, strictInputTypes: true, @@ -241,7 +242,8 @@ runInEachFileSystem(() => { ); }); - it('should support one input property mapping to multiple fields', () => { + /** This is not supported at runtime */ + xit('should support one input property mapping to multiple fields', () => { env.write( 'test.ts', ` diff --git a/packages/language-service/test/grp1/diagnostic_spec.ts b/packages/language-service/test/grp1/diagnostic_spec.ts index 94f738b1ead..e53984914b5 100644 --- a/packages/language-service/test/grp1/diagnostic_spec.ts +++ b/packages/language-service/test/grp1/diagnostic_spec.ts @@ -59,7 +59,7 @@ describe('getSemanticDiagnostics', () => { expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`); }); - it('produces diagnostic for duplicate docarated property rather than crashing', () => { + it('produces diagnostic for duplicate decorated property rather than crashing', () => { const files = { 'app.ts': ` import {Component, Input} from '@angular/core'; @@ -82,9 +82,7 @@ describe('getSemanticDiagnostics', () => { expect(diags[0].messageText).toBe(`Duplicate identifier 'test1'.`); expect(diags[1].category).toBe(ts.DiagnosticCategory.Error); expect(diags[1].file?.fileName).toBe('/test/app.ts'); - expect(diags[1].messageText).toBe( - `Duplicate decorated properties found on class 'AppComponent': test1`, - ); + expect(diags[1].messageText).toBe(`Input 'test1' is bound to both 'test1' and 'test1'.`); }); it('should process external template', () => {