mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(compiler): throw on duplicate input/outputs
inputs & outputs cannot be binded to 2 different directives/components properties
Eg
```
data = model();
dataChange = output(); // throws because model already emits on the `dataChange` output
userSomething = input({alias 'user'});
user = input(); // throws because userSomething already binds to the `user` input
````
fixes #65844
BREAKING CHANGE: The compiler will throw when there a when inputs, outputs or model are binding to the same input/outputs.
This commit is contained in:
committed by
Jessica Janiuk
parent
3bc095d508
commit
03db2aefaa
@@ -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,
|
||||
|
||||
@@ -1346,6 +1346,7 @@ function parseInputFields(
|
||||
emitDeclarationOnly: boolean,
|
||||
): Record<string, InputMapping> {
|
||||
const inputs = {} as Record<string, InputMapping>;
|
||||
const bindings = new Map<string, ClassMember>();
|
||||
|
||||
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<string, string>,
|
||||
): Record<string, string> {
|
||||
const outputs = {} as Record<string, string>;
|
||||
const bindings = new Map<string, ClassMember>();
|
||||
|
||||
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 (
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<boolean>;
|
||||
_blaDecorator: EventEmitter<void>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<TestDir, never>;
|
||||
static ɵdir: i0.ɵɵDirectiveDeclaration<TestDir, never, never, {}, { "click1": "click1"; "click2": "click2"; "click3": "click3"; "_bla": "decoratorPublicName"; "_bla2": "decoratorPublicName2"; "clickDecorator1": "clickDecorator1"; "clickDecorator2": "clickDecorator2"; "_blaDecorator": "decoratorPublicName"; }, never, never, true, never>;
|
||||
static ɵdir: i0.ɵɵDirectiveDeclaration<TestDir, never, never, {}, { "click1": "click1"; "click2": "click2"; "click3": "click3"; "_bla": "decoratorPublicName"; "_bla2": "decoratorPublicName2"; "clickDecorator1": "clickDecorator1"; "clickDecorator2": "clickDecorator2"; "_blaDecorator": "decoratorPublicName3"; }, never, never, true, never>;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export class TestDir {
|
||||
_bla2: "decoratorPublicName2",
|
||||
clickDecorator1: "clickDecorator1",
|
||||
clickDecorator2: "clickDecorator2",
|
||||
_blaDecorator: "decoratorPublicName"
|
||||
_blaDecorator: "decoratorPublicName3"
|
||||
}
|
||||
…
|
||||
});
|
||||
|
||||
@@ -11,5 +11,5 @@ export class TestDir {
|
||||
|
||||
@Output() clickDecorator1 = new EventEmitter();
|
||||
@Output() clickDecorator2 = new EventEmitter<boolean>();
|
||||
@Output('decoratorPublicName') _blaDecorator = new EventEmitter<void>();
|
||||
@Output('decoratorPublicName3') _blaDecorator = new EventEmitter<void>();
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
`
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user