Skip to content

Commit 58d0647

Browse files
crisbetopterratpro
authored andcommitted
refactor(core): add host directive definitions validation (angular#47589)
Adds some logic to ensure that host directives are configured correctly. PR Close angular#47589
1 parent 202a58f commit 58d0647

File tree

5 files changed

+464
-5
lines changed

5 files changed

+464
-5
lines changed

goldens/public-api/core/errors.md

+12
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,24 @@ export const enum RuntimeErrorCode {
2525
// (undocumented)
2626
CYCLIC_DI_DEPENDENCY = -200,
2727
// (undocumented)
28+
DUPLICATE_DIRECTITVE = 309,
29+
// (undocumented)
2830
ERROR_HANDLER_NOT_FOUND = 402,
2931
// (undocumented)
3032
EXPORT_NOT_FOUND = -301,
3133
// (undocumented)
3234
EXPRESSION_CHANGED_AFTER_CHECKED = -100,
3335
// (undocumented)
36+
HOST_DIRECTIVE_COMPONENT = 310,
37+
// (undocumented)
38+
HOST_DIRECTIVE_CONFLICTING_ALIAS = 312,
39+
// (undocumented)
40+
HOST_DIRECTIVE_NOT_STANDALONE = 308,
41+
// (undocumented)
42+
HOST_DIRECTIVE_UNDEFINED_BINDING = 311,
43+
// (undocumented)
44+
HOST_DIRECTIVE_UNRESOLVABLE = 307,
45+
// (undocumented)
3446
IMPORT_PROVIDERS_FROM_STANDALONE = 800,
3547
// (undocumented)
3648
INJECTOR_ALREADY_DESTROYED = 205,

packages/core/src/errors.ts

+6
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ export const enum RuntimeErrorCode {
4242
UNKNOWN_ELEMENT = 304,
4343
TEMPLATE_STRUCTURE_ERROR = 305,
4444
INVALID_EVENT_BINDING = 306,
45+
HOST_DIRECTIVE_UNRESOLVABLE = 307,
46+
HOST_DIRECTIVE_NOT_STANDALONE = 308,
47+
DUPLICATE_DIRECTITVE = 309,
48+
HOST_DIRECTIVE_COMPONENT = 310,
49+
HOST_DIRECTIVE_UNDEFINED_BINDING = 311,
50+
HOST_DIRECTIVE_CONFLICTING_ALIAS = 312,
4551

4652
// Bootstrap Errors
4753
MULTIPLE_PLATFORMS = 400,

packages/core/src/render3/features/host_directives_feature.ts

+83-3
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import {resolveForwardRef} from '../../di';
9+
import {RuntimeError, RuntimeErrorCode} from '../../errors';
910
import {Type} from '../../interface/type';
1011
import {EMPTY_OBJ} from '../../util/empty';
11-
import {getDirectiveDef} from '../definition';
12-
import {DirectiveDef, HostDirectiveBindingMap, HostDirectiveDefs} from '../interfaces/definition';
12+
import {getComponentDef, getDirectiveDef} from '../definition';
13+
import {DirectiveDef, HostDirectiveBindingMap, HostDirectiveDef, HostDirectiveDefs} from '../interfaces/definition';
1314

1415
/** Values that can be used to define a host directive through the `HostDirectivesFeature`. */
1516
type HostDirectiveConfig = Type<unknown>|{
@@ -62,7 +63,9 @@ function findHostDirectiveDefs(
6263
for (const hostDirectiveConfig of currentDef.hostDirectives) {
6364
const hostDirectiveDef = getDirectiveDef(hostDirectiveConfig.directive)!;
6465

65-
// TODO(crisbeto): assert that the def exists.
66+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
67+
validateHostDirective(hostDirectiveConfig, hostDirectiveDef, matchedDefs);
68+
}
6669

6770
// Host directives execute before the host so that its host bindings can be overwritten.
6871
findHostDirectiveDefs(hostDirectiveDef, matchedDefs, hostDirectiveDefs);
@@ -89,3 +92,80 @@ function bindingArrayToMap(bindings: string[]|undefined): HostDirectiveBindingMa
8992

9093
return result;
9194
}
95+
96+
/**
97+
* Verifies that the host directive has been configured correctly.
98+
* @param hostDirectiveConfig Host directive configuration object.
99+
* @param directiveDef Directive definition of the host directive.
100+
* @param matchedDefs Directives that have been matched so far.
101+
*/
102+
function validateHostDirective(
103+
hostDirectiveConfig: HostDirectiveDef<unknown>, directiveDef: DirectiveDef<any>|null,
104+
matchedDefs: DirectiveDef<unknown>[]): asserts directiveDef is DirectiveDef<unknown> {
105+
// TODO(crisbeto): implement more of these checks in the compiler.
106+
const type = hostDirectiveConfig.directive;
107+
108+
if (directiveDef === null) {
109+
if (getComponentDef(type) !== null) {
110+
throw new RuntimeError(
111+
RuntimeErrorCode.HOST_DIRECTIVE_COMPONENT,
112+
`Host directive ${type.name} cannot be a component.`);
113+
}
114+
115+
throw new RuntimeError(
116+
RuntimeErrorCode.HOST_DIRECTIVE_UNRESOLVABLE,
117+
`Could not resolve metadata for host directive ${type.name}. ` +
118+
`Make sure that the ${type.name} class is annotated with an @Directive decorator.`);
119+
}
120+
121+
if (!directiveDef.standalone) {
122+
throw new RuntimeError(
123+
RuntimeErrorCode.HOST_DIRECTIVE_NOT_STANDALONE,
124+
`Host directive ${directiveDef.type.name} must be standalone.`);
125+
}
126+
127+
if (matchedDefs.indexOf(directiveDef) > -1) {
128+
throw new RuntimeError(
129+
RuntimeErrorCode.DUPLICATE_DIRECTITVE,
130+
`Directive ${directiveDef.type.name} matches multiple times on the same element. ` +
131+
`Directives can only match an element once.`);
132+
}
133+
134+
validateMappings('input', directiveDef, hostDirectiveConfig.inputs);
135+
validateMappings('output', directiveDef, hostDirectiveConfig.outputs);
136+
}
137+
138+
/**
139+
* Checks that the host directive inputs/outputs configuration is valid.
140+
* @param bindingType Kind of binding that is being validated. Used in the error message.
141+
* @param def Definition of the host directive that is being validated against.
142+
* @param hostDirectiveDefs Host directive mapping object that shold be validated.
143+
*/
144+
function validateMappings(
145+
bindingType: 'input'|'output', def: DirectiveDef<unknown>,
146+
hostDirectiveDefs: HostDirectiveBindingMap) {
147+
const className = def.type.name;
148+
const bindings: Record<string, string> = bindingType === 'input' ? def.inputs : def.outputs;
149+
150+
for (const publicName in hostDirectiveDefs) {
151+
if (hostDirectiveDefs.hasOwnProperty(publicName)) {
152+
if (!bindings.hasOwnProperty(publicName)) {
153+
throw new RuntimeError(
154+
RuntimeErrorCode.HOST_DIRECTIVE_UNDEFINED_BINDING,
155+
`Directive ${className} does not have an ${bindingType} with a public name of ${
156+
publicName}.`);
157+
}
158+
159+
const remappedPublicName = hostDirectiveDefs[publicName];
160+
161+
if (bindings.hasOwnProperty(remappedPublicName) &&
162+
bindings[remappedPublicName] !== publicName) {
163+
throw new RuntimeError(
164+
RuntimeErrorCode.HOST_DIRECTIVE_CONFLICTING_ALIAS,
165+
`Cannot alias ${bindingType} ${publicName} of host directive ${className} to ${
166+
remappedPublicName}, because it already has a different ${
167+
bindingType} with the same public name.`);
168+
}
169+
}
170+
}
171+
}

packages/core/src/render3/instructions/shared.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} f
2525
import {throwMultipleComponentError} from '../errors';
2626
import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} from '../hooks';
2727
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS} from '../interfaces/container';
28-
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, HostBindingsFunction, HostDirectiveDefs, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
28+
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, HostBindingsFunction, HostDirectiveBindingMap, HostDirectiveDefs, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
2929
import {NodeInjectorFactory} from '../interfaces/injector';
3030
import {getUniqueLViewId} from '../interfaces/lview_tracking';
3131
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode} from '../interfaces/node';
@@ -873,7 +873,7 @@ export function createTNode(
873873
function generatePropertyAliases(
874874
aliasMap: {[publicName: string]: string}, directiveIndex: number,
875875
propertyAliases: PropertyAliases|null,
876-
hostDirectiveAliasMap: {[internalName: string]: string}|null): PropertyAliases|null {
876+
hostDirectiveAliasMap: HostDirectiveBindingMap|null): PropertyAliases|null {
877877
for (let publicName in aliasMap) {
878878
if (aliasMap.hasOwnProperty(publicName)) {
879879
propertyAliases = propertyAliases === null ? {} : propertyAliases;

0 commit comments

Comments
 (0)