Skip to content

Commit a1c34c6

Browse files
alxhubatscott
authored andcommitted
fix(compiler): correct confusion between field and property names (#38685)
The `R3TargetBinder` accepts an interface for directive metadata which declares types for `input` and `output` objects. These types convey the mapping between the property names for an input or output and the corresponding property name on the component class. Due to `R3TargetBinder`'s requirements, this mapping was specified with property names as keys and field names as values. However, because of duck typing, this interface was accidentally satisifed by the opposite mapping, of field names to property names, that was produced in other parts of the compiler. This form more naturally represents the data model for inputs. Rather than accept the field -> property mapping and invert it, this commit introduces a new abstraction for such mappings which is bidirectional, eliminating the ambiguous plain object type. This mapping uses new, unambiguous terminology ("class property name" and "binding property name") and can be used to satisfy both the needs of the binder as well as those of the template type-checker (field -> property). A new test ensures that the input/output metadata produced by the compiler during analysis is directly compatible with the binder via this unambiguous new interface. PR Close #38685
1 parent b084bff commit a1c34c6

File tree

20 files changed

+399
-110
lines changed

20 files changed

+399
-110
lines changed

packages/compiler-cli/src/ngtsc/annotations/src/component.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {absoluteFrom, relative} from '../../file_system';
1515
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
1616
import {DependencyTracker} from '../../incremental/api';
1717
import {IndexingContext} from '../../indexer';
18-
import {DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
18+
import {ClassPropertyMapping, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
1919
import {flattenInheritedDirectiveMetadata} from '../../metadata/src/inheritance';
2020
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
2121
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
@@ -56,6 +56,9 @@ export interface ComponentAnalysisData {
5656
template: ParsedTemplateWithSource;
5757
metadataStmt: Statement|null;
5858

59+
inputs: ClassPropertyMapping;
60+
outputs: ClassPropertyMapping;
61+
5962
/**
6063
* Providers extracted from the `providers` field of the component annotation which will require
6164
* an Angular factory definition at runtime.
@@ -191,7 +194,7 @@ export class ComponentDecoratorHandler implements
191194
}
192195

193196
// Next, read the `@Component`-specific fields.
194-
const {decorator: component, metadata} = directiveResult;
197+
const {decorator: component, metadata, inputs, outputs} = directiveResult;
195198

196199
// Go through the root directories for this project, and select the one with the smallest
197200
// relative path representation.
@@ -328,6 +331,8 @@ export class ComponentDecoratorHandler implements
328331
const output: AnalysisOutput<ComponentAnalysisData> = {
329332
analysis: {
330333
baseClass: readBaseClass(node, this.reflector, this.evaluator),
334+
inputs,
335+
outputs,
331336
meta: {
332337
...metadata,
333338
template: {
@@ -345,7 +350,7 @@ export class ComponentDecoratorHandler implements
345350
i18nUseExternalIds: this.i18nUseExternalIds,
346351
relativeContextFilePath,
347352
},
348-
typeCheckMeta: extractDirectiveTypeCheckMeta(node, metadata.inputs, this.reflector),
353+
typeCheckMeta: extractDirectiveTypeCheckMeta(node, inputs, this.reflector),
349354
metadataStmt: generateSetClassMetadataCall(
350355
node, this.reflector, this.defaultImportRecorder, this.isCore,
351356
this.annotateForClosureCompiler),
@@ -370,8 +375,8 @@ export class ComponentDecoratorHandler implements
370375
name: node.name.text,
371376
selector: analysis.meta.selector,
372377
exportAs: analysis.meta.exportAs,
373-
inputs: analysis.meta.inputs,
374-
outputs: analysis.meta.outputs,
378+
inputs: analysis.inputs,
379+
outputs: analysis.outputs,
375380
queries: analysis.meta.queries.map(query => query.propertyName),
376381
isComponent: true,
377382
baseClass: analysis.baseClass,

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

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as ts from 'typescript';
1111

1212
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
1313
import {DefaultImportRecorder, Reference} from '../../imports';
14-
import {DirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
14+
import {ClassPropertyMapping, DirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
1515
import {extractDirectiveTypeCheckMeta} from '../../metadata/src/util';
1616
import {DynamicValue, EnumValue, PartialEvaluator} from '../../partial_evaluator';
1717
import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
@@ -39,6 +39,8 @@ export interface DirectiveHandlerData {
3939
meta: R3DirectiveMetadata;
4040
metadataStmt: Statement|null;
4141
providersRequiringFactory: Set<Reference<ClassDeclaration>>|null;
42+
inputs: ClassPropertyMapping;
43+
outputs: ClassPropertyMapping;
4244
}
4345

4446
export class DirectiveDecoratorHandler implements
@@ -83,11 +85,10 @@ export class DirectiveDecoratorHandler implements
8385
const directiveResult = extractDirectiveMetadata(
8486
node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore,
8587
flags, this.annotateForClosureCompiler);
86-
const analysis = directiveResult && directiveResult.metadata;
87-
88-
if (analysis === undefined) {
88+
if (directiveResult === undefined) {
8989
return {};
9090
}
91+
const analysis = directiveResult.metadata;
9192

9293
let providersRequiringFactory: Set<Reference<ClassDeclaration>>|null = null;
9394
if (directiveResult !== undefined && directiveResult.decorator.has('providers')) {
@@ -97,12 +98,14 @@ export class DirectiveDecoratorHandler implements
9798

9899
return {
99100
analysis: {
101+
inputs: directiveResult.inputs,
102+
outputs: directiveResult.outputs,
100103
meta: analysis,
101104
metadataStmt: generateSetClassMetadataCall(
102105
node, this.reflector, this.defaultImportRecorder, this.isCore,
103106
this.annotateForClosureCompiler),
104107
baseClass: readBaseClass(node, this.reflector, this.evaluator),
105-
typeCheckMeta: extractDirectiveTypeCheckMeta(node, analysis.inputs, this.reflector),
108+
typeCheckMeta: extractDirectiveTypeCheckMeta(node, directiveResult.inputs, this.reflector),
106109
providersRequiringFactory
107110
}
108111
};
@@ -117,8 +120,8 @@ export class DirectiveDecoratorHandler implements
117120
name: node.name.text,
118121
selector: analysis.meta.selector,
119122
exportAs: analysis.meta.exportAs,
120-
inputs: analysis.meta.inputs,
121-
outputs: analysis.meta.outputs,
123+
inputs: analysis.inputs,
124+
outputs: analysis.outputs,
122125
queries: analysis.meta.queries.map(query => query.propertyName),
123126
isComponent: false,
124127
baseClass: analysis.baseClass,
@@ -199,8 +202,13 @@ export class DirectiveDecoratorHandler implements
199202
export function extractDirectiveMetadata(
200203
clazz: ClassDeclaration, decorator: Readonly<Decorator|null>, reflector: ReflectionHost,
201204
evaluator: PartialEvaluator, defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
202-
flags: HandlerFlags, annotateForClosureCompiler: boolean, defaultSelector: string|null = null):
203-
{decorator: Map<string, ts.Expression>, metadata: R3DirectiveMetadata}|undefined {
205+
flags: HandlerFlags, annotateForClosureCompiler: boolean,
206+
defaultSelector: string|null = null): {
207+
decorator: Map<string, ts.Expression>,
208+
metadata: R3DirectiveMetadata,
209+
inputs: ClassPropertyMapping,
210+
outputs: ClassPropertyMapping,
211+
}|undefined {
204212
let directive: Map<string, ts.Expression>;
205213
if (decorator === null || decorator.args === null || decorator.args.length === 0) {
206214
directive = new Map<string, ts.Expression>();
@@ -331,15 +339,18 @@ export function extractDirectiveMetadata(
331339
const type = wrapTypeReference(reflector, clazz);
332340
const internalType = new WrappedNodeExpr(reflector.getInternalNameOfClass(clazz));
333341

342+
const inputs = ClassPropertyMapping.fromMappedObject({...inputsFromMeta, ...inputsFromFields});
343+
const outputs = ClassPropertyMapping.fromMappedObject({...outputsFromMeta, ...outputsFromFields});
344+
334345
const metadata: R3DirectiveMetadata = {
335346
name: clazz.name.text,
336347
deps: ctorDeps,
337348
host,
338349
lifecycle: {
339350
usesOnChanges,
340351
},
341-
inputs: {...inputsFromMeta, ...inputsFromFields},
342-
outputs: {...outputsFromMeta, ...outputsFromFields},
352+
inputs: inputs.toJointMappedObject(),
353+
outputs: outputs.toDirectMappedObject(),
343354
queries,
344355
viewQueries,
345356
selector,
@@ -352,7 +363,12 @@ export function extractDirectiveMetadata(
352363
exportAs,
353364
providers
354365
};
355-
return {decorator: directive, metadata};
366+
return {
367+
decorator: directive,
368+
metadata,
369+
inputs,
370+
outputs,
371+
};
356372
}
357373

358374
export function extractQueryMetadata(

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8+
import {CssSelector, DirectiveMeta as T2DirectiveMeta, parseTemplate, R3TargetBinder, SelectorMatcher, TmplAstElement} from '@angular/compiler';
89
import * as ts from 'typescript';
910

1011
import {absoluteFrom} from '../../file_system';
@@ -73,6 +74,49 @@ runInEachFileSystem(() => {
7374
expect(span.start.toString()).toContain('/entry.ts@5:22');
7475
expect(span.end.toString()).toContain('/entry.ts@5:29');
7576
});
77+
78+
it('should produce metadata compatible with template binding', () => {
79+
const src = `
80+
import {Directive, Input} from '@angular/core';
81+
82+
@Directive({selector: '[dir]'})
83+
export class TestDir {
84+
@Input('propName')
85+
fieldName: string;
86+
}
87+
`;
88+
const {program} = makeProgram([
89+
{
90+
name: _('/node_modules/@angular/core/index.d.ts'),
91+
contents: 'export const Directive: any; export const Input: any;',
92+
},
93+
{
94+
name: _('/entry.ts'),
95+
contents: src,
96+
},
97+
]);
98+
99+
const analysis = analyzeDirective(program, 'TestDir');
100+
const matcher = new SelectorMatcher<T2DirectiveMeta>();
101+
const dirMeta: T2DirectiveMeta = {
102+
exportAs: null,
103+
inputs: analysis.inputs,
104+
outputs: analysis.outputs,
105+
isComponent: false,
106+
name: 'Dir',
107+
};
108+
matcher.addSelectables(CssSelector.parse('[dir]'), dirMeta);
109+
110+
const {nodes} = parseTemplate('<div dir [propName]="expr"></div>', 'unimportant.html');
111+
const binder = new R3TargetBinder(matcher).bind({template: nodes});
112+
const propBinding = (nodes[0] as TmplAstElement).inputs[0];
113+
const propBindingConsumer = binder.getConsumerOfBinding(propBinding);
114+
115+
// Assert that the consumer of the binding is the directive, which means that the metadata
116+
// fed into the SelectorMatcher was compatible with the binder, and did not confuse property
117+
// and field names.
118+
expect(propBindingConsumer).toBe(dirMeta);
119+
});
76120
});
77121

78122
// Helpers

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as ts from 'typescript';
1111

1212
import {absoluteFrom, AbsoluteFsPath} from '../../file_system';
1313
import {Reference} from '../../imports';
14+
import {ClassPropertyMapping} from '../../metadata';
1415
import {ClassDeclaration} from '../../reflection';
1516
import {getDeclaration, makeProgram} from '../../testing';
1617
import {ComponentMeta} from '../src/context';
@@ -50,8 +51,8 @@ export function getBoundTemplate(
5051
selector,
5152
name: declaration.name.getText(),
5253
isComponent: true,
53-
inputs: {},
54-
outputs: {},
54+
inputs: ClassPropertyMapping.fromMappedObject({}),
55+
outputs: ClassPropertyMapping.fromMappedObject({}),
5556
exportAs: null,
5657
});
5758
});

packages/compiler-cli/src/ngtsc/metadata/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export * from './src/api';
1010
export {DtsMetadataReader} from './src/dts';
1111
export {CompoundMetadataRegistry, LocalMetadataRegistry, InjectableClassRegistry} from './src/registry';
1212
export {extractDirectiveTypeCheckMeta, CompoundMetadataReader} from './src/util';
13+
export {BindingPropertyName, ClassPropertyMapping, ClassPropertyName, InputOrOutput} from './src/property_mapping';

packages/compiler-cli/src/ngtsc/metadata/src/api.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import * as ts from 'typescript';
1212
import {Reference} from '../../imports';
1313
import {ClassDeclaration} from '../../reflection';
1414

15+
import {ClassPropertyMapping, ClassPropertyName} from './property_mapping';
16+
1517

1618
/**
1719
* Metadata collected for an `NgModule`.
@@ -52,25 +54,25 @@ export interface DirectiveTypeCheckMeta {
5254
* Directive's class. This allows inputs to accept a wider range of types and coerce the input to
5355
* a narrower type with a getter/setter. See https://angular.io/guide/template-typecheck.
5456
*/
55-
coercedInputFields: Set<string>;
57+
coercedInputFields: Set<ClassPropertyName>;
5658

5759
/**
5860
* The set of input fields which map to `readonly`, `private`, or `protected` members in the
5961
* Directive's class.
6062
*/
61-
restrictedInputFields: Set<string>;
63+
restrictedInputFields: Set<ClassPropertyName>;
6264

6365
/**
6466
* The set of input fields which are declared as string literal members in the Directive's class.
6567
* We need to track these separately because these fields may not be valid JS identifiers so
6668
* we cannot use them with property access expressions when assigning inputs.
6769
*/
68-
stringLiteralInputFields: Set<string>;
70+
stringLiteralInputFields: Set<ClassPropertyName>;
6971

7072
/**
7173
* The set of input fields which do not have corresponding members in the Directive's class.
7274
*/
73-
undeclaredInputFields: Set<string>;
75+
undeclaredInputFields: Set<ClassPropertyName>;
7476

7577
/**
7678
* Whether the Directive's class is generic, i.e. `class MyDir<T> {...}`.
@@ -89,6 +91,16 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta {
8991
selector: string|null;
9092
queries: string[];
9193

94+
/**
95+
* A mapping of input field names to the property names.
96+
*/
97+
inputs: ClassPropertyMapping;
98+
99+
/**
100+
* A mapping of output field names to the property names.
101+
*/
102+
outputs: ClassPropertyMapping;
103+
92104
/**
93105
* A `Reference` to the base class for the directive, if one was detected.
94106
*

packages/compiler-cli/src/ngtsc/metadata/src/dts.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {Reference} from '../../imports';
1212
import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost} from '../../reflection';
1313

1414
import {DirectiveMeta, MetadataReader, NgModuleMeta, PipeMeta} from './api';
15+
import {ClassPropertyMapping} from './property_mapping';
1516
import {extractDirectiveTypeCheckMeta, extractReferencesFromType, readStringArrayType, readStringMapType, readStringType} from './util';
1617

1718
/**
@@ -76,15 +77,18 @@ export class DtsMetadataReader implements MetadataReader {
7677
return null;
7778
}
7879

79-
const inputs = readStringMapType(def.type.typeArguments[3]);
80+
const inputs =
81+
ClassPropertyMapping.fromMappedObject(readStringMapType(def.type.typeArguments[3]));
82+
const outputs =
83+
ClassPropertyMapping.fromMappedObject(readStringMapType(def.type.typeArguments[4]));
8084
return {
8185
ref,
8286
name: clazz.name.text,
8387
isComponent: def.name === 'ɵcmp',
8488
selector: readStringType(def.type.typeArguments[1]),
8589
exportAs: readStringArrayType(def.type.typeArguments[2]),
8690
inputs,
87-
outputs: readStringMapType(def.type.typeArguments[4]),
91+
outputs,
8892
queries: readStringArrayType(def.type.typeArguments[5]),
8993
...extractDirectiveTypeCheckMeta(clazz, inputs, this.reflector),
9094
baseClass: readBaseClass(clazz, this.checker, this.reflector),

packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
*/
88

99
import {Reference} from '../../imports';
10-
import {DirectiveMeta, MetadataReader} from '../../metadata';
1110
import {ClassDeclaration} from '../../reflection';
1211

12+
import {DirectiveMeta, MetadataReader} from './api';
13+
import {ClassPropertyMapping, ClassPropertyName} from './property_mapping';
14+
1315
/**
1416
* Given a reference to a directive, return a flattened version of its `DirectiveMeta` metadata
1517
* which includes metadata from its entire inheritance chain.
@@ -25,13 +27,13 @@ export function flattenInheritedDirectiveMetadata(
2527
throw new Error(`Metadata not found for directive: ${dir.debugName}`);
2628
}
2729

28-
let inputs: {[key: string]: string|[string, string]} = {};
29-
let outputs: {[key: string]: string} = {};
30-
const coercedInputFields = new Set<string>();
31-
const undeclaredInputFields = new Set<string>();
32-
const restrictedInputFields = new Set<string>();
33-
const stringLiteralInputFields = new Set<string>();
30+
const coercedInputFields = new Set<ClassPropertyName>();
31+
const undeclaredInputFields = new Set<ClassPropertyName>();
32+
const restrictedInputFields = new Set<ClassPropertyName>();
33+
const stringLiteralInputFields = new Set<ClassPropertyName>();
3434
let isDynamic = false;
35+
let inputs = ClassPropertyMapping.empty();
36+
let outputs = ClassPropertyMapping.empty();
3537

3638
const addMetadata = (meta: DirectiveMeta): void => {
3739
if (meta.baseClass === 'dynamic') {
@@ -45,8 +47,9 @@ export function flattenInheritedDirectiveMetadata(
4547
isDynamic = true;
4648
}
4749
}
48-
inputs = {...inputs, ...meta.inputs};
49-
outputs = {...outputs, ...meta.outputs};
50+
51+
inputs = ClassPropertyMapping.merge(inputs, meta.inputs);
52+
outputs = ClassPropertyMapping.merge(outputs, meta.outputs);
5053

5154
for (const coercedInputField of meta.coercedInputFields) {
5255
coercedInputFields.add(coercedInputField);

0 commit comments

Comments
 (0)