refactor(compiler-cli): introduce NG8023 compile-time diagnostic for duplicate selectors

Add NG8023 extended diagnostic to report duplicate component selectors
during compilation.

This replaces the former NG0300 runtime error, ensuring the failure
occurs at build time instead of runtime.

Closes  angular#48377

BREAKING CHANGE: Elements with multiple matching selectors will now throw at compile time.
This commit is contained in:
SkyZeroZx
2026-03-19 12:39:42 -05:00
committed by Leon Senft
parent eb53392b10
commit ca67828ee2
7 changed files with 173 additions and 6 deletions
@@ -1450,7 +1450,7 @@ runInEachFileSystem((os: string) => {
class DirectiveA {}
@Component({
selector: 'comp',
selector: 'comp-a',
template: '...',
standalone: false,
})
@@ -1471,7 +1471,7 @@ runInEachFileSystem((os: string) => {
class DirectiveB {}
@Component({
selector: 'comp',
selector: 'comp-b',
template: '...',
standalone: false,
})
@@ -1481,7 +1481,8 @@ runInEachFileSystem((os: string) => {
selector: 'app',
template: \`
<div dir></div>
<comp></comp>
<comp-a></comp-a>
<comp-b></comp-b>
\`,
standalone: false,
})
@@ -1556,7 +1557,7 @@ runInEachFileSystem((os: string) => {
class DirectiveA {}
@Component({
selector: 'comp',
selector: 'comp-a',
template: '...',
standalone: false,
})
@@ -1577,7 +1578,7 @@ runInEachFileSystem((os: string) => {
class DirectiveB {}
@Component({
selector: 'comp',
selector: 'comp-b',
template: '...',
standalone: false,
})
@@ -1595,7 +1596,8 @@ runInEachFileSystem((os: string) => {
selector: 'app',
template: \`
<div dir></div>
<comp></comp>
<comp-a></comp-a>
<comp-b></comp-b>
\`,
standalone: false,
})
@@ -8791,5 +8791,115 @@ suppress
expect(diags.length).toBe(0);
});
});
describe('multiple matching components', () => {
it('should report an error when multiple components match the same element', () => {
env.tsconfig({strictTemplates: true});
env.write(
'test.ts',
`
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-comp',
template: '',
standalone: false,
})
export class CompA {}
@Component({
selector: 'my-comp',
template: '',
standalone: false,
})
export class CompB {}
@Component({
selector: 'test',
template: '<my-comp />',
standalone: false,
})
export class TestCmp {}
@NgModule({
declarations: [TestCmp, CompA, CompB],
})
export class Module {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.MULTIPLE_MATCHING_COMPONENTS));
expect(diags[0].messageText).toContain(
'Multiple components match node with tagname my-comp',
);
});
it('should report an error when multiple components with attribute selectors match the same element', () => {
env.tsconfig({strictTemplates: true});
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
selector: '[stroked-button]',
template: '',
})
export class StrokedBtn {}
@Component({
selector: '[raised-button]',
template: '',
})
export class RaisedBtn {}
@Component({
selector: 'app-root',
template: '<button stroked-button raised-button></button>',
imports: [StrokedBtn, RaisedBtn],
})
export class App {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.MULTIPLE_MATCHING_COMPONENTS));
expect(diags[0].messageText).toContain(
'Multiple components match node with tagname button',
);
});
it('should not report an error when a single component and directives match', () => {
env.tsconfig({strictTemplates: true});
env.write(
'test.ts',
`
import {Component, Directive} from '@angular/core';
@Component({
selector: 'my-comp',
template: '',
})
export class CompA {}
@Directive({
selector: 'my-comp',
})
export class DirB {}
@Component({
selector: 'test',
template: '<my-comp />',
imports: [CompA, DirB],
})
export class TestCmp {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
});
});
});