Skip to content

Commit 57432f1

Browse files
crisbetoatscott
authored andcommitted
refactor(compiler-cli): merge duplicate directive matches and report conflicts
Implements the logic at the compiler level that will de-duplicate host directives and merge them together. It will also report if a conflict is detected during merging.
1 parent 22f9ee1 commit 57432f1

15 files changed

Lines changed: 961 additions & 29 deletions

File tree

goldens/public-api/compiler-cli/error_code.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export enum ErrorCode {
2828
CONFIG_FLAT_MODULE_NO_INDEX = 4001,
2929
// (undocumented)
3030
CONFIG_STRICT_TEMPLATES_IMPLIES_FULL_TEMPLATE_TYPECHECK = 4002,
31+
CONFLICTING_HOST_DIRECTIVE_BINDING = 8024,
3132
CONFLICTING_INPUT_TRANSFORM = 2020,
3233
CONFLICTING_LET_DECLARATION = 8017,
3334
CONTROL_FLOW_PREVENTING_CONTENT_PROJECTION = 8011,

packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ runInEachFileSystem(() => {
128128
const analysis = analyzeDirective(program, 'TestDir');
129129
const matcher = new SelectorMatcher<T2DirectiveMeta[]>();
130130
const dirMeta: T2DirectiveMeta = {
131+
ref: {
132+
key: 'TestDir',
133+
},
131134
exportAs: null,
132135
inputs: analysis.inputs,
133136
outputs: analysis.outputs,

packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,11 @@ export enum ErrorCode {
449449
*/
450450
MULTIPLE_MATCHING_COMPONENTS = 8023,
451451

452+
/**
453+
* Raised when a host directive input/output is exposed multiple times under the same name.
454+
*/
455+
CONFLICTING_HOST_DIRECTIVE_BINDING = 8024,
456+
452457
/**
453458
* A two way binding in a template has an incorrect syntax,
454459
* parentheses outside brackets. For example:

packages/compiler-cli/src/ngtsc/imports/src/references.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ export class Reference<T extends ts.Node = ts.Node> {
8282
if (sourceFile) {
8383
this.key = `${sourceFile.fileName}#${node.getStart()}`;
8484
} else {
85-
this.key = `${this.bestGuessOwningModule?.specifier}#${id?.text}#${id?.getStart()}#${id?.getEnd()}`;
85+
// `getStart` will throw if there's no source file.
86+
const position = id && id.getSourceFile() ? id.getStart() : null;
87+
this.key = `${this.bestGuessOwningModule?.specifier}#${id?.text}#${position}`;
8688
}
8789
}
8890

packages/compiler-cli/src/ngtsc/indexer/test/util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {
1010
BoundTarget,
11+
ClassPropertyMapping,
1112
CssSelector,
1213
DirectiveMatcher,
1314
MatchSource,
@@ -21,7 +22,6 @@ import ts from 'typescript';
2122

2223
import {absoluteFrom, AbsoluteFsPath} from '../../file_system';
2324
import {Reference} from '../../imports';
24-
import {ClassPropertyMapping} from '../../metadata';
2525
import {ClassDeclaration} from '../../reflection';
2626
import {getDeclaration, makeProgram} from '../../testing';
2727
import {ComponentMeta} from '../src/context';

packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@ import ts from 'typescript';
1010

1111
import {Reference, ReferenceEmitter} from '../../imports';
1212
import {
13-
ClassPropertyMapping,
1413
CompoundMetadataRegistry,
1514
DirectiveMeta,
1615
LocalMetadataRegistry,
17-
MatchSource,
1816
MetadataRegistry,
1917
MetaKind,
2018
PipeMeta,
@@ -23,6 +21,7 @@ import {ClassDeclaration} from '../../reflection';
2321
import {LocalModuleScope, ScopeData} from '../src/api';
2422
import {DtsModuleScopeResolver} from '../src/dependency';
2523
import {LocalModuleScopeRegistry} from '../src/local';
24+
import {ClassPropertyMapping, MatchSource} from '@angular/compiler';
2625

2726
function registerFakeRefs(registry: MetadataRegistry): {
2827
[name: string]: Reference<ClassDeclaration>;

packages/compiler-cli/src/ngtsc/typecheck/api/oob.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,4 +244,22 @@ export interface OutOfBandDiagnosticRecorder<T> {
244244
element: TmplAstElement,
245245
componentNames: string[],
246246
): void;
247+
248+
/**
249+
* Reports that a host directive input/output has been exposed under multiple names.
250+
* @param id Type checking ID of the template in which the host directive was used.
251+
* @param node Node on which the host directive was used.
252+
* @param directiveName Name of the host directive.
253+
* @param kind Type of the conflicting binding.
254+
* @param classPropertyName Name of the class member that declares the input/output.
255+
* @param aliases Aliases under which the binding is exposed.
256+
*/
257+
conflictingHostDirectiveBinding(
258+
id: TypeCheckId,
259+
node: TmplAstElement | TmplAstTemplate | TmplAstComponent | TmplAstDirective,
260+
directiveName: string,
261+
kind: 'input' | 'output',
262+
classPropertyName: string,
263+
aliases: string[],
264+
): void;
247265
}

packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -347,33 +347,11 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor
347347
isComponent ? 'component' : 'directive'
348348
} ${directiveName} must be specified.`;
349349

350-
let span: ParseSourceSpan;
351-
let name: string | null;
352-
353-
if (element instanceof TmplAstElement || element instanceof TmplAstDirective) {
354-
name = element.name;
355-
} else if (element instanceof TmplAstComponent) {
356-
name = element.componentName;
357-
} else {
358-
name = null;
359-
}
360-
361-
if (name === null) {
362-
span = element.startSourceSpan;
363-
} else {
364-
// Only highlight the tag name since highlighting the entire start tag can be noisy.
365-
const start = element.startSourceSpan.start.moveBy(1);
366-
const end = element.startSourceSpan.end.moveBy(
367-
start.offset + name.length - element.startSourceSpan.end.offset,
368-
);
369-
span = new ParseSourceSpan(start, end);
370-
}
371-
372350
this._diagnostics.push(
373351
makeTemplateDiagnostic(
374352
id,
375353
this.resolver.getTemplateSourceMapping(id),
376-
span,
354+
this.getTagNameSpan(element),
377355
ts.DiagnosticCategory.Error,
378356
ngErrorCode(ErrorCode.MISSING_REQUIRED_INPUTS),
379357
message,
@@ -694,6 +672,59 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor
694672
),
695673
);
696674
}
675+
676+
conflictingHostDirectiveBinding(
677+
id: TypeCheckId,
678+
node: TmplAstElement | TmplAstTemplate | TmplAstComponent | TmplAstDirective,
679+
directiveName: string,
680+
kind: 'input' | 'output',
681+
classPropertyName: string,
682+
aliases: string[],
683+
): void {
684+
const message =
685+
`${kind === 'input' ? 'Input' : 'Output'} declared in ${directiveName}.${classPropertyName} ` +
686+
`is exposed under the following conflicting names: ${aliases.map((a) => `"${a}"`).join(', ')}. ` +
687+
`An ${kind} can only be exposed under a single name.`;
688+
689+
this._diagnostics.push(
690+
makeTemplateDiagnostic(
691+
id,
692+
this.resolver.getTemplateSourceMapping(id),
693+
this.getTagNameSpan(node),
694+
ts.DiagnosticCategory.Error,
695+
ngErrorCode(ErrorCode.CONFLICTING_HOST_DIRECTIVE_BINDING),
696+
message,
697+
),
698+
);
699+
}
700+
701+
private getTagNameSpan(
702+
node: TmplAstElement | TmplAstTemplate | TmplAstComponent | TmplAstDirective,
703+
) {
704+
let span: ParseSourceSpan;
705+
let name: string | null;
706+
707+
if (node instanceof TmplAstElement || node instanceof TmplAstDirective) {
708+
name = node.name;
709+
} else if (node instanceof TmplAstComponent) {
710+
name = node.componentName;
711+
} else {
712+
name = null;
713+
}
714+
715+
if (name === null) {
716+
span = node.startSourceSpan;
717+
} else {
718+
// Only highlight the tag name since highlighting the entire start tag can be noisy.
719+
const start = node.startSourceSpan.start.moveBy(1);
720+
const end = node.startSourceSpan.end.moveBy(
721+
start.offset + name.length - node.startSourceSpan.end.offset,
722+
);
723+
span = new ParseSourceSpan(start, end);
724+
}
725+
726+
return span;
727+
}
697728
}
698729

699730
function translateCategory(category: OutOfBadDiagnosticCategory): ts.DiagnosticCategory {

packages/compiler-cli/src/ngtsc/typecheck/src/ops/scope.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,8 @@ export class Scope {
579579
return;
580580
}
581581

582+
this.reportConflictingBindings(node);
583+
582584
if (node instanceof TmplAstElement) {
583585
const isDeferred = this.tcb.boundTarget.isDeferred(node);
584586
if (!isDeferred && directives.some((dirMeta) => dirMeta.isExplicitlyDeferred)) {
@@ -688,6 +690,8 @@ export class Scope {
688690
this.directiveOpMap.set(node, dirMap);
689691
}
690692

693+
this.reportConflictingBindings(node);
694+
691695
// In selectorless all directive inputs have to be claimed.
692696
if (node instanceof TmplAstDirective) {
693697
for (const input of node.inputs) {
@@ -1043,4 +1047,23 @@ export class Scope {
10431047
);
10441048
}
10451049
}
1050+
1051+
private reportConflictingBindings(
1052+
node: TmplAstElement | TmplAstTemplate | TmplAstComponent | TmplAstDirective,
1053+
): void {
1054+
const conflictingBindings = this.tcb.boundTarget.getConflictingHostDirectiveBindings(node);
1055+
1056+
if (conflictingBindings !== null) {
1057+
for (const binding of conflictingBindings) {
1058+
this.tcb.oobRecorder.conflictingHostDirectiveBinding(
1059+
this.tcb.id,
1060+
node,
1061+
binding.directive.name,
1062+
binding.kind,
1063+
binding.classPropertyName,
1064+
Array.from(binding.conflictingAliases),
1065+
);
1066+
}
1067+
}
1068+
}
10461069
}

packages/compiler-cli/src/ngtsc/typecheck/src/tcb_adapter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
TmplAstTemplate,
3131
WrappedNodeExpr,
3232
ClassPropertyMapping,
33+
ConflictingHostDirectiveBinding,
3334
} from '@angular/compiler';
3435
import {InputMapping} from '../../metadata';
3536
import {requiresInlineTypeCtor} from './type_constructor';
@@ -176,6 +177,10 @@ export function adaptTypeCheckBlockMetadata(
176177
getEntitiesInScope: (node) => meta.boundTarget.getEntitiesInScope(node),
177178
getEagerlyUsedPipes: () => meta.boundTarget.getEagerlyUsedPipes(),
178179
getDeferBlocks: () => meta.boundTarget.getDeferBlocks(),
180+
getConflictingHostDirectiveBindings: (node) =>
181+
meta.boundTarget.getConflictingHostDirectiveBindings(node) as
182+
| ConflictingHostDirectiveBinding<TcbDirectiveMetadata>[]
183+
| null,
179184
};
180185

181186
const pipes = new Map<string, TcbPipeMetadata>();

0 commit comments

Comments
 (0)