Skip to content

Commit be97c87

Browse files
crisbetoalxhub
authored andcommitted
refactor(compiler): required inputs prerequisite refactors (#49333)
Based on the discussion in #49304 (comment). Reworks the compiler internals to allow for additional information about inputs to be stored. This is a prerequisite for required inputs. PR Close #49333
1 parent 0814f20 commit be97c87

File tree

28 files changed

+406
-233
lines changed

28 files changed

+406
-233
lines changed

packages/compiler-cli/linker/src/ast/ast_value.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,12 @@ export class AstObject<T extends object, TExpression> {
156156
* Converts the AstObject to a raw JavaScript object, mapping each property value (as an
157157
* `AstValue`) to the generic type (`T`) via the `mapper` function.
158158
*/
159-
toLiteral<V>(mapper: (value: AstValue<ObjectValueType<T>, TExpression>) => V): Record<string, V> {
159+
toLiteral<V>(mapper: (value: AstValue<ObjectValueType<T>, TExpression>, key: string) => V):
160+
Record<string, V> {
160161
const result: Record<string, V> = {};
161162
for (const [key, expression] of this.obj) {
162-
result[key] = mapper(new AstValue<ObjectValueType<T>, TExpression>(expression, this.host));
163+
result[key] =
164+
mapper(new AstValue<ObjectValueType<T>, TExpression>(expression, this.host), key);
163165
}
164166
return result;
165167
}

packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_directive_linker_1.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,11 @@ export function toR3DirectiveMeta<TExpression>(
8080
/**
8181
* Decodes the AST value for a single input to its representation as used in the metadata.
8282
*/
83-
function toInputMapping<TExpression>(value: AstValue<string|[string, string], TExpression>):
84-
string|[string, string] {
83+
function toInputMapping<TExpression>(
84+
value: AstValue<string|[string, string], TExpression>,
85+
key: string): {bindingPropertyName: string, classPropertyName: string} {
8586
if (value.isString()) {
86-
return value.getString();
87+
return {bindingPropertyName: value.getString(), classPropertyName: key};
8788
}
8889

8990
const values = value.getArray().map(innerValue => innerValue.getString());
@@ -92,7 +93,7 @@ function toInputMapping<TExpression>(value: AstValue<string|[string, string], TE
9293
value.expression,
9394
'Unsupported input, expected a string or an array containing exactly two strings');
9495
}
95-
return values as [string, string];
96+
return {bindingPropertyName: values[0], classPropertyName: values[1]};
9697
}
9798

9899
/**

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {ExtendedTemplateChecker} from '../../../typecheck/extended/api';
2727
import {getSourceFile} from '../../../util/src/typescript';
2828
import {Xi18nContext} from '../../../xi18n';
2929
import {combineResolvers, compileDeclareFactory, compileNgFactoryDefField, compileResults, extractClassMetadata, extractSchemas, findAngularDecorator, forwardRefResolver, getDirectiveDiagnostics, getProviderDiagnostics, InjectableClassRegistry, isExpressionForwardReference, readBaseClass, resolveEnumValue, resolveImportedFile, resolveLiteral, resolveProvidersRequiringFactory, ResourceLoader, toFactoryMetadata, validateHostDirectives, wrapFunctionExpressionsInParens,} from '../../common';
30-
import {extractDirectiveMetadata, parseFieldArrayValue} from '../../directive';
30+
import {extractDirectiveMetadata, parseFieldStringArrayValue} from '../../directive';
3131
import {createModuleWithProvidersResolver, NgModuleSymbol} from '../../ng_module';
3232

3333
import {checkCustomElementSelectorForErrors, makeCyclicImportInfo} from './diagnostics';
@@ -36,7 +36,6 @@ import {_extractTemplateStyleUrls, extractComponentStyleUrls, extractStyleResour
3636
import {ComponentSymbol} from './symbol';
3737
import {animationTriggerResolver, collectAnimationNames, validateAndFlattenComponentImports} from './util';
3838

39-
const EMPTY_MAP = new Map<string, Expression>();
4039
const EMPTY_ARRAY: any[] = [];
4140

4241
/**
@@ -153,7 +152,7 @@ export class ComponentDecoratorHandler implements
153152
// Extract inline styles, process, and cache for use in synchronous analyze phase
154153
let inlineStyles;
155154
if (component.has('styles')) {
156-
const litStyles = parseFieldArrayValue(component, 'styles', this.evaluator);
155+
const litStyles = parseFieldStringArrayValue(component, 'styles', this.evaluator);
157156
if (litStyles === null) {
158157
this.preanalyzeStylesCache.set(node, null);
159158
} else {
@@ -403,7 +402,7 @@ export class ComponentDecoratorHandler implements
403402
}
404403

405404
if (component.has('styles')) {
406-
const litStyles = parseFieldArrayValue(component, 'styles', this.evaluator);
405+
const litStyles = parseFieldStringArrayValue(component, 'styles', this.evaluator);
407406
if (litStyles !== null) {
408407
inlineStyles = [...litStyles];
409408
styles.push(...litStyles);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {AnimationTriggerNames, R3ClassMetadata, R3ComponentMetadata, R3TemplateD
1010
import ts from 'typescript';
1111

1212
import {Reference} from '../../../imports';
13-
import {ClassPropertyMapping, ComponentResources, DirectiveTypeCheckMeta, HostDirectiveMeta} from '../../../metadata';
13+
import {ClassPropertyMapping, ComponentResources, DirectiveTypeCheckMeta, HostDirectiveMeta, InputMapping} from '../../../metadata';
1414
import {ClassDeclaration} from '../../../reflection';
1515
import {SubsetOfKeys} from '../../../util/src/typescript';
1616

@@ -36,7 +36,7 @@ export interface ComponentAnalysisData {
3636
template: ParsedTemplateWithSource;
3737
classMetadata: R3ClassMetadata|null;
3838

39-
inputs: ClassPropertyMapping;
39+
inputs: ClassPropertyMapping<InputMapping>;
4040
outputs: ClassPropertyMapping;
4141

4242
/**

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

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

1212
import {Reference, ReferenceEmitter} from '../../../imports';
1313
import {extractSemanticTypeParameters, SemanticDepGraphUpdater} from '../../../incremental/semantic_graph';
14-
import {ClassPropertyMapping, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, HostDirectiveMeta, MatchSource, MetadataReader, MetadataRegistry, MetaKind} from '../../../metadata';
14+
import {ClassPropertyMapping, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, HostDirectiveMeta, InputMapping, MatchSource, MetadataReader, MetadataRegistry, MetaKind} from '../../../metadata';
1515
import {PartialEvaluator} from '../../../partial_evaluator';
1616
import {PerfEvent, PerfRecorder} from '../../../perf';
1717
import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, ReflectionHost} from '../../../reflection';
@@ -37,7 +37,7 @@ export interface DirectiveHandlerData {
3737
meta: R3DirectiveMetadata;
3838
classMetadata: R3ClassMetadata|null;
3939
providersRequiringFactory: Set<Reference<ClassDeclaration>>|null;
40-
inputs: ClassPropertyMapping;
40+
inputs: ClassPropertyMapping<InputMapping>;
4141
outputs: ClassPropertyMapping;
4242
isPoisoned: boolean;
4343
isStructural: boolean;

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

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

1212
import {ErrorCode, FatalDiagnosticError} from '../../../diagnostics';
1313
import {Reference, ReferenceEmitter} from '../../../imports';
14-
import {ClassPropertyMapping, HostDirectiveMeta} from '../../../metadata';
14+
import {ClassPropertyMapping, HostDirectiveMeta, InputMapping} from '../../../metadata';
1515
import {DynamicValue, EnumValue, PartialEvaluator, ResolvedValue} from '../../../partial_evaluator';
1616
import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral} from '../../../reflection';
1717
import {HandlerFlags} from '../../../transform';
@@ -37,7 +37,7 @@ export function extractDirectiveMetadata(
3737
annotateForClosureCompiler: boolean, defaultSelector: string|null = null): {
3838
decorator: Map<string, ts.Expression>,
3939
metadata: R3DirectiveMetadata,
40-
inputs: ClassPropertyMapping,
40+
inputs: ClassPropertyMapping<InputMapping>,
4141
outputs: ClassPropertyMapping,
4242
isStructural: boolean;
4343
hostDirectives: HostDirectiveMeta[] | null, rawHostDirectives: ts.Expression | null,
@@ -74,19 +74,18 @@ export function extractDirectiveMetadata(
7474
const coreModule = isCore ? undefined : '@angular/core';
7575

7676
// Construct the map of inputs both from the @Directive/@Component
77-
// decorator, and the decorated
78-
// fields.
79-
const inputsFromMeta = parseFieldToPropertyMapping(directive, 'inputs', evaluator);
80-
const inputsFromFields = parseDecoratedFields(
81-
filterToMembersWithDecorator(decoratedElements, 'Input', coreModule), evaluator,
82-
resolveInput);
77+
// decorator, and the decorated fields.
78+
const inputsFromMeta = parseInputsArray(directive, evaluator);
79+
const inputsFromFields = parseInputFields(
80+
filterToMembersWithDecorator(decoratedElements, 'Input', coreModule), evaluator);
81+
const inputs = ClassPropertyMapping.fromMappedObject({...inputsFromMeta, ...inputsFromFields});
8382

8483
// And outputs.
85-
const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', evaluator);
86-
const outputsFromFields =
87-
parseDecoratedFields(
88-
filterToMembersWithDecorator(decoratedElements, 'Output', coreModule), evaluator,
89-
resolveOutput) as {[field: string]: string};
84+
const outputsFromMeta = parseOutputsArray(directive, evaluator);
85+
const outputsFromFields = parseOutputFields(
86+
filterToMembersWithDecorator(decoratedElements, 'Output', coreModule), evaluator);
87+
const outputs = ClassPropertyMapping.fromMappedObject({...outputsFromMeta, ...outputsFromFields});
88+
9089
// Construct the list of queries.
9190
const contentChildFromFields = queriesFromFields(
9291
filterToMembersWithDecorator(decoratedElements, 'ContentChild', coreModule), reflector,
@@ -185,8 +184,6 @@ export function extractDirectiveMetadata(
185184
const sourceFile = clazz.getSourceFile();
186185
const type = wrapTypeReference(reflector, clazz);
187186
const internalType = new WrappedNodeExpr(reflector.getInternalNameOfClass(clazz));
188-
const inputs = ClassPropertyMapping.fromMappedObject({...inputsFromMeta, ...inputsFromFields});
189-
const outputs = ClassPropertyMapping.fromMappedObject({...outputsFromMeta, ...outputsFromFields});
190187
const rawHostDirectives = directive.get('hostDirectives') || null;
191188
const hostDirectives =
192189
rawHostDirectives === null ? null : extractHostDirectives(rawHostDirectives, evaluator);
@@ -448,7 +445,7 @@ function extractQueriesFromDecorator(
448445
return {content, view};
449446
}
450447

451-
export function parseFieldArrayValue(
448+
export function parseFieldStringArrayValue(
452449
directive: Map<string, ts.Expression>, field: string, evaluator: PartialEvaluator): null|
453450
string[] {
454451
if (!directive.has(field)) {
@@ -512,76 +509,138 @@ function isPropertyTypeMember(member: ClassMember): boolean {
512509
member.kind === ClassMemberKind.Property;
513510
}
514511

515-
/**
516-
* Interpret property mapping fields on the decorator (e.g. inputs or outputs) and return the
517-
* correctly shaped metadata object.
518-
*/
519-
function parseFieldToPropertyMapping(
520-
directive: Map<string, ts.Expression>, field: string,
521-
evaluator: PartialEvaluator): {[field: string]: string} {
522-
const metaValues = parseFieldArrayValue(directive, field, evaluator);
523-
return metaValues ? parseInputOutputMappingArray(metaValues) : EMPTY_OBJECT;
524-
}
525-
526-
function parseInputOutputMappingArray(values: string[]) {
512+
function parseMappingStringArray(values: string[]) {
527513
return values.reduce((results, value) => {
528514
if (typeof value !== 'string') {
529515
throw new Error('Mapping value must be a string');
530516
}
531517

532-
// Either the value is 'field' or 'field: property'. In the first case, `property` will
533-
// be undefined, in which case the field name should also be used as the property name.
534-
const [field, property] = value.split(':', 2).map(str => str.trim());
535-
results[field] = property || field;
518+
const [bindingPropertyName, fieldName] = parseMappingString(value);
519+
results[fieldName] = bindingPropertyName;
536520
return results;
537521
}, {} as {[field: string]: string});
538522
}
539523

524+
function parseMappingString(value: string): [bindingPropertyName: string, fieldName: string] {
525+
// Either the value is 'field' or 'field: property'. In the first case, `property` will
526+
// be undefined, in which case the field name should also be used as the property name.
527+
const [fieldName, bindingPropertyName] = value.split(':', 2).map(str => str.trim());
528+
return [bindingPropertyName ?? fieldName, fieldName];
529+
}
530+
540531
/**
541-
* Parse property decorators (e.g. `Input` or `Output`) and return the correctly shaped metadata
542-
* object.
532+
* Parse property decorators (e.g. `Input` or `Output`) and invoke callback with the parsed data.
543533
*/
544534
function parseDecoratedFields(
545535
fields: {member: ClassMember, decorators: Decorator[]}[], evaluator: PartialEvaluator,
546-
mapValueResolver: (publicName: string, internalName: string) =>
547-
string | [string, string]): {[field: string]: string|[string, string]} {
548-
return fields.reduce((results, field) => {
536+
callback: (fieldName: string, fieldValue: ResolvedValue, decorator: Decorator) => void): void {
537+
for (const field of fields) {
549538
const fieldName = field.member.name;
550-
field.decorators.forEach(decorator => {
551-
// The decorator either doesn't have an argument (@Input()) in which case the property
552-
// name is used, or it has one argument (@Output('named')).
553-
if (decorator.args == null || decorator.args.length === 0) {
554-
results[fieldName] = fieldName;
555-
} else if (decorator.args.length === 1) {
556-
const property = evaluator.evaluate(decorator.args[0]);
557-
if (typeof property !== 'string') {
558-
throw createValueHasWrongTypeError(
559-
Decorator.nodeForError(decorator), property,
560-
`@${decorator.name} decorator argument must resolve to a string`);
561-
}
562-
results[fieldName] = mapValueResolver(property, fieldName);
563-
} else {
564-
// Too many arguments.
539+
540+
for (const decorator of field.decorators) {
541+
if (decorator.args != null && decorator.args.length > 1) {
565542
throw new FatalDiagnosticError(
566543
ErrorCode.DECORATOR_ARITY_WRONG, Decorator.nodeForError(decorator),
567544
`@${decorator.name} can have at most one argument, got ${
568545
decorator.args.length} argument(s)`);
569546
}
570-
});
571-
return results;
572-
}, {} as {[field: string]: string | [string, string]});
547+
548+
const value = decorator.args != null && decorator.args.length > 0 ?
549+
evaluator.evaluate(decorator.args[0]) :
550+
null;
551+
552+
callback(fieldName, value, decorator);
553+
}
554+
}
573555
}
574556

575-
function resolveInput(publicName: string, internalName: string): [string, string] {
576-
return [publicName, internalName];
557+
/** Parses the `inputs` array of a directive/component decorator. */
558+
function parseInputsArray(
559+
decoratorMetadata: Map<string, ts.Expression>,
560+
evaluator: PartialEvaluator): Record<string, InputMapping> {
561+
const inputsField = decoratorMetadata.get('inputs');
562+
563+
if (inputsField === undefined) {
564+
return {};
565+
}
566+
567+
const inputs = {} as Record<string, InputMapping>;
568+
const inputsArray = evaluator.evaluate(inputsField);
569+
570+
// TODO(required-inputs): change the error wording here
571+
if (!Array.isArray(inputsArray)) {
572+
throw createValueHasWrongTypeError(
573+
inputsField, inputsArray, `Failed to resolve @Directive.inputs to a string array`);
574+
}
575+
576+
for (const value of inputsArray) {
577+
// TODO(required-inputs): parse object-based config.
578+
if (typeof value === 'string') {
579+
// If the value is a string, we treat it as a mapping string.
580+
const [bindingPropertyName, fieldName] = parseMappingString(value);
581+
inputs[fieldName] = {bindingPropertyName, classPropertyName: fieldName, required: false};
582+
} else {
583+
// TODO(required-inputs): change the error wording here
584+
throw createValueHasWrongTypeError(
585+
inputsField, value, `Failed to resolve @Directive.inputs to a string array`);
586+
}
587+
}
588+
589+
return inputs;
577590
}
578591

579-
function resolveOutput(publicName: string, internalName: string) {
580-
return publicName;
592+
/** Parses the class members that are decorated as inputs. */
593+
function parseInputFields(
594+
inputMembers: {member: ClassMember, decorators: Decorator[]}[],
595+
evaluator: PartialEvaluator): Record<string, InputMapping> {
596+
const inputs = {} as Record<string, InputMapping>;
597+
598+
parseDecoratedFields(inputMembers, evaluator, (classPropertyName, options, decorator) => {
599+
let bindingPropertyName: string;
600+
601+
// TODO(required-inputs): parse object-based config.
602+
if (options === null) {
603+
bindingPropertyName = classPropertyName;
604+
} else if (typeof options === 'string') {
605+
bindingPropertyName = options;
606+
} else {
607+
// TODO(required-inputs): change the error wording here
608+
throw createValueHasWrongTypeError(
609+
Decorator.nodeForError(decorator), options,
610+
`@${decorator.name} decorator argument must resolve to a string`);
611+
}
612+
613+
inputs[classPropertyName] = {bindingPropertyName, classPropertyName, required: false};
614+
});
615+
616+
return inputs;
617+
}
618+
619+
/** Parses the `outputs` array of a directive/component. */
620+
function parseOutputsArray(
621+
directive: Map<string, ts.Expression>, evaluator: PartialEvaluator): Record<string, string> {
622+
const metaValues = parseFieldStringArrayValue(directive, 'outputs', evaluator);
623+
return metaValues ? parseMappingStringArray(metaValues) : EMPTY_OBJECT;
624+
}
625+
626+
/** Parses the class members that are decorated as outputs. */
627+
function parseOutputFields(
628+
outputMembers: {member: ClassMember, decorators: Decorator[]}[],
629+
evaluator: PartialEvaluator): Record<string, string> {
630+
const outputs = {} as Record<string, string>;
631+
632+
parseDecoratedFields(outputMembers, evaluator, (fieldName, bindingPropertyName, decorator) => {
633+
if (bindingPropertyName != null && typeof bindingPropertyName !== 'string') {
634+
throw createValueHasWrongTypeError(
635+
Decorator.nodeForError(decorator), bindingPropertyName,
636+
`@${decorator.name} decorator argument must resolve to a string`);
637+
}
638+
639+
outputs[fieldName] = bindingPropertyName ?? fieldName;
640+
});
641+
642+
return outputs;
581643
}
582-
type StringMap<T> = {
583-
[key: string]: T;
584-
};
585644

586645
function evaluateHostExpressionBindings(
587646
hostExpr: ts.Expression, evaluator: PartialEvaluator): ParsedHostBindings {
@@ -590,7 +649,7 @@ function evaluateHostExpressionBindings(
590649
throw createValueHasWrongTypeError(
591650
hostExpr, hostMetaMap, `Decorator host metadata must be an object`);
592651
}
593-
const hostMetadata: StringMap<string|Expression> = {};
652+
const hostMetadata: Record<string, string|Expression> = {};
594653
hostMetaMap.forEach((value, key) => {
595654
// Resolve Enum references to their declared value.
596655
if (value instanceof EnumValue) {
@@ -673,13 +732,13 @@ function extractHostDirectives(
673732
*/
674733
function parseHostDirectivesMapping(
675734
field: 'inputs'|'outputs', resolvedValue: ResolvedValue, classReference: ClassDeclaration,
676-
sourceExpression: ts.Expression): {[publicName: string]: string}|null {
735+
sourceExpression: ts.Expression): {[bindingPropertyName: string]: string}|null {
677736
if (resolvedValue instanceof Map && resolvedValue.has(field)) {
678737
const nameForErrors = `@Directive.hostDirectives.${classReference.name.text}.${field}`;
679738
const rawInputs = resolvedValue.get(field);
680739

681740
if (isStringArrayOrDie(rawInputs, nameForErrors, sourceExpression)) {
682-
return parseInputOutputMappingArray(rawInputs);
741+
return parseMappingStringArray(rawInputs);
683742
}
684743
}
685744

0 commit comments

Comments
 (0)