Skip to content

Commit ab98b24

Browse files
committed
fix(compiler-cli): capture metadata for undecorated fields (angular#63957)
Currently if `TestBed.overrideComponent` is used on a class that uses initializer APIs (e.g. `input()`), the initializer metadata will be wiped out, because `overrideComponent` re-compiles the class with the information set by `setClassMetadata`. `setClassMetadata` only captures decorated members at the moment. These changes introduce some logic to capture the new initializer-based APIs in `setClassMetadata` as well. Fixes angular#57944. PR Close angular#63957
1 parent f28355a commit ab98b24

File tree

16 files changed

+419
-29
lines changed

16 files changed

+419
-29
lines changed

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import {
3030

3131
import {valueReferenceToExpression, wrapFunctionExpressionsInParens} from './util';
3232

33+
/** Function that extracts metadata from an undercorated class member. */
34+
export type UndecoratedMetadataExtractor = (member: ClassMember) => LiteralArrayExpr | null;
35+
3336
/**
3437
* Given a class declaration, generate a call to `setClassMetadata` with the Angular metadata
3538
* present on the class or its member fields. An ngDevMode guard is used to allow the call to be
@@ -44,6 +47,7 @@ export function extractClassMetadata(
4447
isCore: boolean,
4548
annotateForClosureCompiler?: boolean,
4649
angularDecoratorTransform: (dec: Decorator) => Decorator = (dec) => dec,
50+
undecoratedMetadataExtractor: UndecoratedMetadataExtractor = () => null,
4751
): R3ClassMetadata | null {
4852
if (!reflection.isClass(clazz)) {
4953
return null;
@@ -98,10 +102,12 @@ export function extractClassMetadata(
98102
let duplicateDecoratedMembers: ClassMember[] | null = null;
99103

100104
for (const member of classMembers) {
105+
const shouldQuoteName = member.nameNode !== null && ts.isStringLiteralLike(member.nameNode);
106+
101107
if (member.decorators !== null && member.decorators.length > 0) {
102108
decoratedMembers.push({
103109
key: member.name,
104-
quoted: false,
110+
quoted: shouldQuoteName,
105111
value: decoratedClassMemberToMetadata(member.decorators!, isCore),
106112
});
107113

@@ -111,6 +117,16 @@ export function extractClassMetadata(
111117
} else {
112118
seenMemberNames.add(member.name);
113119
}
120+
} else {
121+
const undecoratedMetadata = undecoratedMetadataExtractor(member);
122+
123+
if (undecoratedMetadata !== null) {
124+
decoratedMembers.push({
125+
key: member.name,
126+
quoted: shouldQuoteName,
127+
value: undecoratedMetadata,
128+
});
129+
}
114130
}
115131
}
116132

packages/compiler-cli/src/ngtsc/annotations/common/test/metadata_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ runInEachFileSystem(() => {
108108
}
109109
`);
110110
expect(res).toContain(
111-
`{ 'has-dashes-in-name': [{ type: Input }], noDashesInName: [{ type: Input }] })`,
111+
`{ "has-dashes-in-name": [{ type: Input }], noDashesInName: [{ type: Input }] })`,
112112
);
113113
});
114114

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,14 @@ import {
154154
ResourceLoader,
155155
toFactoryMetadata,
156156
tryUnwrapForwardRef,
157+
UndecoratedMetadataExtractor,
157158
validateHostDirectives,
158159
wrapFunctionExpressionsInParens,
159160
} from '../../common';
160161
import {
161162
extractDirectiveMetadata,
162163
extractHostBindingResources,
164+
getDirectiveUndecoratedMetadataExtractor,
163165
parseDirectiveStyles,
164166
} from '../../directive';
165167
import {createModuleWithProvidersResolver, NgModuleSymbol} from '../../ng_module';
@@ -290,6 +292,11 @@ export class ComponentDecoratorHandler
290292
preserveSignificantWhitespace: this.i18nPreserveSignificantWhitespace,
291293
};
292294

295+
this.undecoratedMetadataExtractor = getDirectiveUndecoratedMetadataExtractor(
296+
reflector,
297+
importTracker,
298+
);
299+
293300
// Dependencies can't be deferred during HMR, because the HMR update module can't have
294301
// dynamic imports and its dependencies need to be passed in directly. If dependencies
295302
// are deferred, their imports will be deleted so we may lose the reference to them.
@@ -298,6 +305,7 @@ export class ComponentDecoratorHandler
298305

299306
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
300307
private elementSchemaRegistry = new DomElementSchemaRegistry();
308+
private readonly undecoratedMetadataExtractor: UndecoratedMetadataExtractor;
301309

302310
/**
303311
* During the asynchronous preanalyze phase, it's necessary to parse the template to extract
@@ -973,6 +981,7 @@ export class ComponentDecoratorHandler
973981
this.isCore,
974982
this.annotateForClosureCompiler,
975983
(dec) => transformDecoratorResources(dec, component, styles, template),
984+
this.undecoratedMetadataExtractor,
976985
)
977986
: null,
978987
classDebugInfo: extractClassDebugInfo(

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,16 @@ import {
7474
ReferencesRegistry,
7575
resolveProvidersRequiringFactory,
7676
toFactoryMetadata,
77+
UndecoratedMetadataExtractor,
7778
validateHostDirectives,
7879
} from '../../common';
7980

80-
import {extractDirectiveMetadata, extractHostBindingResources, HostBindingNodes} from './shared';
81+
import {
82+
extractDirectiveMetadata,
83+
extractHostBindingResources,
84+
getDirectiveUndecoratedMetadataExtractor,
85+
HostBindingNodes,
86+
} from './shared';
8187
import {DirectiveSymbol} from './symbol';
8288
import {JitDeclarationRegistry} from '../../common/src/jit_declaration_registry';
8389
import {
@@ -154,10 +160,16 @@ export class DirectiveDecoratorHandler
154160
private readonly usePoisonedData: boolean,
155161
private readonly typeCheckHostBindings: boolean,
156162
private readonly emitDeclarationOnly: boolean,
157-
) {}
163+
) {
164+
this.undecoratedMetadataExtractor = getDirectiveUndecoratedMetadataExtractor(
165+
reflector,
166+
importTracker,
167+
);
168+
}
158169

159170
readonly precedence = HandlerPrecedence.PRIMARY;
160171
readonly name = 'DirectiveDecoratorHandler';
172+
private readonly undecoratedMetadataExtractor: UndecoratedMetadataExtractor;
161173

162174
detect(
163175
node: ClassDeclaration,
@@ -240,7 +252,14 @@ export class DirectiveDecoratorHandler
240252
hostDirectives: directiveResult.hostDirectives,
241253
rawHostDirectives: directiveResult.rawHostDirectives,
242254
classMetadata: this.includeClassMetadata
243-
? extractClassMetadata(node, this.reflector, this.isCore, this.annotateForClosureCompiler)
255+
? extractClassMetadata(
256+
node,
257+
this.reflector,
258+
this.isCore,
259+
this.annotateForClosureCompiler,
260+
undefined,
261+
this.undecoratedMetadataExtractor,
262+
)
244263
: null,
245264
baseClass: readBaseClass(node, this.reflector, this.evaluator),
246265
typeCheckMeta: extractDirectiveTypeCheckMeta(node, directiveResult.inputs, this.reflector),

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

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
ExternalReference,
1515
ForwardRefHandling,
1616
getSafePropertyAccessString,
17+
LiteralArrayExpr,
18+
literalMap,
1719
MaybeForwardRefExpression,
1820
ParsedHostBindings,
1921
ParseError,
@@ -24,7 +26,10 @@ import {
2426
R3QueryMetadata,
2527
R3Reference,
2628
verifyHostBindings,
29+
R3Identifiers,
30+
ArrowFunctionExpr,
2731
WrappedNodeExpr,
32+
literal,
2833
} from '@angular/compiler';
2934
import ts from 'typescript';
3035

@@ -76,6 +81,7 @@ import {
7681
ReferencesRegistry,
7782
toR3Reference,
7883
tryUnwrapForwardRef,
84+
UndecoratedMetadataExtractor,
7985
unwrapConstructorDependencies,
8086
unwrapExpression,
8187
validateConstructorDependencies,
@@ -855,6 +861,140 @@ export function parseFieldStringArrayValue(
855861
return value;
856862
}
857863

864+
/**
865+
* Returns a function that can be used to extract data for the `setClassMetadata`
866+
* calls from undecorated directive class members.
867+
*/
868+
export function getDirectiveUndecoratedMetadataExtractor(
869+
reflector: ReflectionHost,
870+
importTracker: ImportedSymbolsTracker,
871+
): UndecoratedMetadataExtractor {
872+
return (member: ClassMember): LiteralArrayExpr | null => {
873+
const input = tryParseSignalInputMapping(member, reflector, importTracker);
874+
if (input !== null) {
875+
return getDecoratorMetaArray([
876+
[new ExternalExpr(R3Identifiers.inputDecorator), memberMetadataFromSignalInput(input)],
877+
]);
878+
}
879+
880+
const output = tryParseInitializerBasedOutput(member, reflector, importTracker);
881+
if (output !== null) {
882+
return getDecoratorMetaArray([
883+
[
884+
new ExternalExpr(R3Identifiers.outputDecorator),
885+
memberMetadataFromInitializerOutput(output.metadata),
886+
],
887+
]);
888+
}
889+
890+
const model = tryParseSignalModelMapping(member, reflector, importTracker);
891+
if (model !== null) {
892+
return getDecoratorMetaArray([
893+
[
894+
new ExternalExpr(R3Identifiers.inputDecorator),
895+
memberMetadataFromSignalInput(model.input),
896+
],
897+
[
898+
new ExternalExpr(R3Identifiers.outputDecorator),
899+
memberMetadataFromInitializerOutput(model.output),
900+
],
901+
]);
902+
}
903+
904+
const query = tryParseSignalQueryFromInitializer(member, reflector, importTracker);
905+
if (query !== null) {
906+
let identifier: ExternalReference;
907+
if (query.name === 'viewChild') {
908+
identifier = R3Identifiers.viewChildDecorator;
909+
} else if (query.name === 'viewChildren') {
910+
identifier = R3Identifiers.viewChildrenDecorator;
911+
} else if (query.name === 'contentChild') {
912+
identifier = R3Identifiers.contentChildDecorator;
913+
} else if (query.name === 'contentChildren') {
914+
identifier = R3Identifiers.contentChildrenDecorator;
915+
} else {
916+
return null;
917+
}
918+
919+
return getDecoratorMetaArray([
920+
[new ExternalExpr(identifier), memberMetadataFromSignalQuery(query.call)],
921+
]);
922+
}
923+
924+
return null;
925+
};
926+
}
927+
928+
function getDecoratorMetaArray(
929+
decorators: [type: ExternalExpr, args: LiteralArrayExpr][],
930+
): LiteralArrayExpr {
931+
return new LiteralArrayExpr(
932+
decorators.map(([type, args]) =>
933+
literalMap([
934+
{key: 'type', value: type, quoted: false},
935+
{key: 'args', value: args, quoted: false},
936+
]),
937+
),
938+
);
939+
}
940+
941+
function memberMetadataFromSignalInput(input: InputMapping): LiteralArrayExpr {
942+
// Note that for signal inputs the transform is captured in the signal
943+
// initializer so we don't need to capture it here.
944+
return new LiteralArrayExpr([
945+
literalMap([
946+
{
947+
key: 'isSignal',
948+
value: literal(true),
949+
quoted: false,
950+
},
951+
{
952+
key: 'alias',
953+
value: literal(input.bindingPropertyName),
954+
quoted: false,
955+
},
956+
{
957+
key: 'required',
958+
value: literal(input.required),
959+
quoted: false,
960+
},
961+
]),
962+
]);
963+
}
964+
965+
function memberMetadataFromInitializerOutput(output: InputOrOutput): LiteralArrayExpr {
966+
return new LiteralArrayExpr([literal(output.bindingPropertyName)]);
967+
}
968+
969+
function memberMetadataFromSignalQuery(call: ts.CallExpression): LiteralArrayExpr {
970+
const firstArg = call.arguments[0];
971+
const firstArgMeta =
972+
ts.isStringLiteralLike(firstArg) || ts.isCallExpression(firstArg)
973+
? new WrappedNodeExpr(firstArg)
974+
: // If the first argument is a class reference, we need to wrap it in a `forwardRef`
975+
// because the reference might occur after the current class. This wouldn't be flagged
976+
// on the query initializer, because it executes after the class is initialized, whereas
977+
// `setClassMetadata` runs immediately.
978+
new ExternalExpr(R3Identifiers.forwardRef).callFn([
979+
new ArrowFunctionExpr([], new WrappedNodeExpr(firstArg)),
980+
]);
981+
982+
const entries: Expression[] = [
983+
// We use wrapped nodes here, because the output AST doesn't support spread assignments.
984+
firstArgMeta,
985+
new WrappedNodeExpr(
986+
ts.factory.createObjectLiteralExpression([
987+
...(call.arguments.length > 1
988+
? [ts.factory.createSpreadAssignment(call.arguments[1])]
989+
: []),
990+
ts.factory.createPropertyAssignment('isSignal', ts.factory.createTrue()),
991+
]),
992+
),
993+
];
994+
995+
return new LiteralArrayExpr(entries);
996+
}
997+
858998
function isStringArrayOrDie(value: any, name: string, node: ts.Expression): value is string[] {
859999
if (!Array.isArray(value)) {
8601000
return false;

packages/compiler-cli/test/compliance/test_cases/model_inputs/GOLDEN_PARTIAL.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ TestDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.
1414
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, decorators: [{
1515
type: Directive,
1616
args: [{}]
17-
}] });
17+
}], propDecorators: { counter: [{ type: i0.Input, args: [{ isSignal: true, alias: "counter", required: false }] }, { type: i0.Output, args: ["counterChange"] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }, { type: i0.Output, args: ["nameChange"] }] } });
1818

1919
/****************************************************************************************************
2020
* PARTIAL FILE: model_directive_definition.d.ts
@@ -45,7 +45,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
4545
args: [{
4646
template: 'Works',
4747
}]
48-
}] });
48+
}], propDecorators: { counter: [{ type: i0.Input, args: [{ isSignal: true, alias: "counter", required: false }] }, { type: i0.Output, args: ["counterChange"] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }, { type: i0.Output, args: ["nameChange"] }] } });
4949

5050
/****************************************************************************************************
5151
* PARTIAL FILE: model_component_definition.d.ts
@@ -80,7 +80,7 @@ TestDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.
8080
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, decorators: [{
8181
type: Directive,
8282
args: [{}]
83-
}], propDecorators: { decoratorInput: [{
83+
}], propDecorators: { counter: [{ type: i0.Input, args: [{ isSignal: true, alias: "counter", required: false }] }, { type: i0.Output, args: ["counterChange"] }], modelWithAlias: [{ type: i0.Input, args: [{ isSignal: true, alias: "alias", required: false }] }, { type: i0.Output, args: ["aliasChange"] }], decoratorInput: [{
8484
type: Input
8585
}], decoratorInputWithAlias: [{
8686
type: Input,

packages/compiler-cli/test/compliance/test_cases/output_function/GOLDEN_PARTIAL.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ TestDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.
1818
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, decorators: [{
1919
type: Directive,
2020
args: [{}]
21-
}] });
21+
}], propDecorators: { a: [{ type: i0.Output, args: ["a"] }], b: [{ type: i0.Output, args: ["b"] }], c: [{ type: i0.Output, args: ["cPublic"] }], d: [{ type: i0.Output, args: ["d"] }], e: [{ type: i0.Output, args: ["e"] }] } });
2222

2323
/****************************************************************************************************
2424
* PARTIAL FILE: output_in_directive.d.ts
@@ -56,7 +56,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
5656
args: [{
5757
template: 'Works',
5858
}]
59-
}] });
59+
}], propDecorators: { a: [{ type: i0.Output, args: ["a"] }], b: [{ type: i0.Output, args: ["b"] }], c: [{ type: i0.Output, args: ["cPublic"] }], d: [{ type: i0.Output, args: ["d"] }], e: [{ type: i0.Output, args: ["e"] }] } });
6060

6161
/****************************************************************************************************
6262
* PARTIAL FILE: output_in_component.d.ts
@@ -94,7 +94,7 @@ TestDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-
9494
TestDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestDir, isStandalone: true, outputs: { click1: "click1", click2: "click2", click3: "click3", _bla: "decoratorPublicName", _bla2: "decoratorPublicName2", clickDecorator1: "clickDecorator1", clickDecorator2: "clickDecorator2", _blaDecorator: "decoratorPublicName" }, ngImport: i0 });
9595
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, decorators: [{
9696
type: Directive
97-
}], propDecorators: { clickDecorator1: [{
97+
}], propDecorators: { click1: [{ type: i0.Output, args: ["click1"] }], click2: [{ type: i0.Output, args: ["click2"] }], click3: [{ type: i0.Output, args: ["click3"] }], _bla: [{ type: i0.Output, args: ["decoratorPublicName"] }], _bla2: [{ type: i0.Output, args: ["decoratorPublicName2"] }], clickDecorator1: [{
9898
type: Output
9999
}], clickDecorator2: [{
100100
type: Output

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -842,13 +842,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
842842
selector: '[hostBindingDir]',
843843
standalone: false
844844
}]
845-
}], propDecorators: { 'is-a': [{
845+
}], propDecorators: { "is-a": [{
846846
type: HostBinding,
847847
args: ['class.a']
848-
}], 'is-"b"': [{
848+
}], "is-\"b\"": [{
849849
type: HostBinding,
850850
args: ['class.b']
851-
}], '"is-c"': [{
851+
}], "\"is-c\"": [{
852852
type: HostBinding,
853853
args: ['class.c']
854854
}] } });

0 commit comments

Comments
 (0)