mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
eb53392b10
commit
ca67828ee2
7 changed files with 173 additions and 6 deletions
|
|
@ -89,6 +89,7 @@ export enum ErrorCode {
|
|||
MISSING_REFERENCE_TARGET = 8003,
|
||||
MISSING_REQUIRED_INPUTS = 8008,
|
||||
MISSING_STRUCTURAL_DIRECTIVE = 8116,
|
||||
MULTIPLE_MATCHING_COMPONENTS = 8023,
|
||||
NGMODULE_BOOTSTRAP_IS_STANDALONE = 6009,
|
||||
NGMODULE_DECLARATION_IS_STANDALONE = 6008,
|
||||
NGMODULE_DECLARATION_NOT_UNIQUE = 6007,
|
||||
|
|
|
|||
|
|
@ -444,6 +444,11 @@ export enum ErrorCode {
|
|||
/** Raised when the user has an unsupported binding on a `FormField` directive. */
|
||||
FORM_FIELD_UNSUPPORTED_BINDING = 8022,
|
||||
|
||||
/**
|
||||
* Raised when multiple components in the compilation scope match a given element in a template.
|
||||
*/
|
||||
MULTIPLE_MATCHING_COMPONENTS = 8023,
|
||||
|
||||
/**
|
||||
* A two way binding in a template has an incorrect syntax,
|
||||
* parentheses outside brackets. For example:
|
||||
|
|
|
|||
|
|
@ -247,6 +247,15 @@ export interface OutOfBandDiagnosticRecorder {
|
|||
id: TypeCheckId,
|
||||
node: TmplAstBoundAttribute | TmplAstTextAttribute,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Reports that multiple components in the compilation scope match a given element.
|
||||
*/
|
||||
multipleMatchingComponents(
|
||||
id: TypeCheckId,
|
||||
element: TmplAstElement,
|
||||
componentNames: string[],
|
||||
): void;
|
||||
}
|
||||
|
||||
export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecorder {
|
||||
|
|
@ -909,6 +918,30 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
multipleMatchingComponents(
|
||||
id: TypeCheckId,
|
||||
element: TmplAstElement,
|
||||
componentNames: string[],
|
||||
): void {
|
||||
const start = element.startSourceSpan.start.moveBy(1);
|
||||
const end = element.startSourceSpan.end.moveBy(
|
||||
start.offset + element.name.length - element.startSourceSpan.end.offset,
|
||||
);
|
||||
const span = new ParseSourceSpan(start, end);
|
||||
const names = componentNames.map((n: string) => `'${n}'`).join(', ');
|
||||
|
||||
this._diagnostics.push(
|
||||
makeTemplateDiagnostic(
|
||||
id,
|
||||
this.resolver.getTemplateSourceMapping(id),
|
||||
span,
|
||||
ts.DiagnosticCategory.Error,
|
||||
ngErrorCode(ErrorCode.MULTIPLE_MATCHING_COMPONENTS),
|
||||
`Multiple components match node with tagname ${element.name}: ${names}.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function makeInlineDiagnostic(
|
||||
|
|
|
|||
|
|
@ -594,6 +594,17 @@ export class Scope {
|
|||
}
|
||||
}
|
||||
|
||||
if (node instanceof TmplAstElement) {
|
||||
const matchedComponents = directives.filter((dir) => dir.isComponent);
|
||||
if (matchedComponents.length > 1) {
|
||||
this.tcb.oobRecorder.multipleMatchingComponents(
|
||||
this.tcb.id,
|
||||
node,
|
||||
matchedComponents.map((dir) => dir.name),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const dirMap = new Map<TcbDirectiveMetadata, number>();
|
||||
for (const dir of directives) {
|
||||
this.appendDirectiveInputs(dir, node, dirMap, directives);
|
||||
|
|
|
|||
|
|
@ -1062,6 +1062,11 @@ export class NoopOobRecorder implements OutOfBandDiagnosticRecorder {
|
|||
target: TmplAstLetDeclaration,
|
||||
): void {}
|
||||
conflictingDeclaration(id: TypeCheckId, current: TmplAstLetDeclaration): void {}
|
||||
multipleMatchingComponents(
|
||||
id: TypeCheckId,
|
||||
element: TmplAstElement,
|
||||
componentNames: string[],
|
||||
): void {}
|
||||
missingNamedTemplateDependency(
|
||||
id: TypeCheckId,
|
||||
node: TmplAstComponent | TmplAstDirective,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue