Skip to content

Commit 5f7db01

Browse files
devversionalxhub
authored andcommitted
refactor(compiler-cli): finalize transform support for signal input transforms (angular#53521)
Signal inputs do not need coercion members for their transforms. That is because the `InputSignal` type- which is accessible in the class member- already holds the type of potential "write values". This eliminates the need for coercion members which were simply used to somehow capture this write type (especially when libraries are consumed and only `.d.ts` is available). We can simplify this, and also significantlky loosen restrictions of transform functions- given that we can fully rely on TypeScript for inferring the type. There is no requirement in being able to "transplant" the type into different places- hence also allowing supporting transform functions with generics, or overloads. In a follow-up commit, once more parts are place, there will be some compliance tests to ensure these new "loosend restrictions". PR Close angular#53521
1 parent 0ef03a5 commit 5f7db01

File tree

10 files changed

+90
-23
lines changed

10 files changed

+90
-23
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export function compileInputTransformFields(inputs: ClassPropertyMapping<InputMa
1717
const extraFields: CompileResult[] = [];
1818

1919
for (const input of inputs) {
20+
// Note: Signal inputs capture their transform `WriteT` as part of the `InputSignal`.
21+
// Such inputs will not have a `transform` captured and not generate coercion members.
22+
2023
if (input.transform) {
2124
extraFields.push({
2225
name: `ngAcceptInputType_${input.classPropertyName}`,

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

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

1212
import {ErrorCode, FatalDiagnosticError, makeRelatedInformation} from '../../../diagnostics';
1313
import {assertSuccessfulReferenceEmit, ImportFlags, Reference, ReferenceEmitter} from '../../../imports';
14-
import {ClassPropertyMapping, HostDirectiveMeta, InputMapping, InputTransform} from '../../../metadata';
14+
import {ClassPropertyMapping, DecoratorInputTransform, HostDirectiveMeta, InputMapping} from '../../../metadata';
1515
import {DynamicValue, EnumValue, PartialEvaluator, ResolvedValue, traceDynamicValue} from '../../../partial_evaluator';
16-
import {AmbientImport, ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral} from '../../../reflection';
16+
import {AmbientImport, ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, FunctionDefinition, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral} from '../../../reflection';
1717
import {CompilationMode} from '../../../transform';
1818
import {createSourceSpan, createValueHasWrongTypeError, forwardRefResolver, getConstructorDependencies, ReferencesRegistry, toR3Reference, tryUnwrapForwardRef, unwrapConstructorDependencies, unwrapExpression, validateConstructorDependencies, wrapFunctionExpressionsInParens, wrapTypeReference,} from '../../common';
1919

@@ -651,7 +651,7 @@ function parseInputsArray(
651651
const name = value.get('name');
652652
const alias = value.get('alias');
653653
const required = value.get('required');
654-
let transform: InputTransform|null = null;
654+
let transform: DecoratorInputTransform|null = null;
655655

656656
if (typeof name !== 'string') {
657657
throw createValueHasWrongTypeError(
@@ -668,7 +668,8 @@ function parseInputsArray(
668668
`Transform of value at position ${i} of @Directive.inputs array must be a function`);
669669
}
670670

671-
transform = parseInputTransformFunction(clazz, name, transformValue, reflector, refEmitter);
671+
transform = parseDecoratorInputTransformFunction(
672+
clazz, name, transformValue, reflector, refEmitter);
672673
}
673674

674675
inputs[name] = {
@@ -746,7 +747,7 @@ function tryParseInputFieldMapping(
746747

747748
const publicInputName = alias ?? classPropertyName;
748749

749-
let transform: InputTransform|null = null;
750+
let transform: DecoratorInputTransform|null = null;
750751
if (options instanceof Map && options.has('transform')) {
751752
const transformValue = options.get('transform');
752753

@@ -755,7 +756,7 @@ function tryParseInputFieldMapping(
755756
optionsNode!, transformValue, `Input transform must be a function`);
756757
}
757758

758-
transform = parseInputTransformFunction(
759+
transform = parseDecoratorInputTransformFunction(
759760
clazz, classPropertyName, transformValue, reflector, refEmitter);
760761
}
761762

@@ -784,7 +785,8 @@ function tryParseInputFieldMapping(
784785
classPropertyName,
785786
bindingPropertyName,
786787
required: signalInput.isRequired,
787-
// TODO: signal inputs- followup.
788+
// Signal inputs do not capture complex transform metadata.
789+
// See more details in the `transform` type of `InputMapping`.
788790
transform: null,
789791
};
790792
}
@@ -821,10 +823,20 @@ function parseInputFields(
821823
return inputs;
822824
}
823825

824-
/** Parses the `transform` function and its type of a specific input. */
825-
export function parseInputTransformFunction(
826+
/**
827+
* Parses the `transform` function and its type for a decorator `@Input`.
828+
*
829+
* This logic verifies feasibility of extracting the transform write type
830+
* into a different place, so that the input write type can be captured at
831+
* a later point in a static acceptance member.
832+
*
833+
* Note: This is not needed for signal inputs where the transform type is
834+
* automatically captured in the type of the `InputSignal`.
835+
*
836+
*/
837+
export function parseDecoratorInputTransformFunction(
826838
clazz: ClassDeclaration, classPropertyName: string, value: DynamicValue|Reference,
827-
reflector: ReflectionHost, refEmitter: ReferenceEmitter): InputTransform {
839+
reflector: ReflectionHost, refEmitter: ReferenceEmitter): DecoratorInputTransform {
828840
const definition = reflector.getDefinitionOfFunction(value.node);
829841

830842
if (definition === null) {

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,35 @@ export enum MatchSource {
134134
/** Metadata for a single input mapping. */
135135
export type InputMapping = InputOrOutput&{
136136
required: boolean;
137-
transform: InputTransform|null
137+
138+
/**
139+
* Transform for the input. Null if no transform is configured.
140+
*
141+
* For signal-based inputs, this is always `null` even if a transform
142+
* is configured. Signal inputs capture their transform write type
143+
* automatically in the `InputSignal`, nor is there a need to emit a
144+
* reference to the transform.
145+
*
146+
* For zone-based decorator `@Input`s this is different because the transform
147+
* write type needs to be captured in a coercion member as the decorator information
148+
* is lost in the `.d.ts` for type-checking.
149+
*/
150+
transform: DecoratorInputTransform|null
138151
};
139152

140-
/** Metadata for an input's transform function. */
141-
export interface InputTransform {
153+
/** Metadata for an `@Input()` transform function. */
154+
export interface DecoratorInputTransform {
155+
/**
156+
* Reference to the transform function so that it can be
157+
* referenced when the input metadata is emitted in the declaration.
158+
*/
142159
node: ts.Node;
160+
/**
161+
* Emittable type for the input transform. Null for signal inputs
162+
*
163+
* This type will be used for inputs to capture the transform type
164+
* for type-checking in corresponding `ngAcceptInputType_` members.
165+
*/
143166
type: Reference<ts.TypeNode>;
144167
}
145168

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ function readInputsType(type: ts.TypeNode): Record<string, InputMapping> {
206206
// Signal inputs were not supported pre v16- so those inputs are never signal based.
207207
isSignal: false,
208208
// Input transform are only tracked for locally-compiled directives. Directives coming
209-
// from the .d.ts already have them included through `ngAcceptInputType` class members.
209+
// from the .d.ts already have them included through `ngAcceptInputType` class members,
210+
// or via the `InputSignal` type of the member.
210211
transform: null,
211212
};
212213
} else {
@@ -220,7 +221,8 @@ function readInputsType(type: ts.TypeNode): Record<string, InputMapping> {
220221
required: config.required,
221222
isSignal: !!config.isSignal,
222223
// Input transform are only tracked for locally-compiled directives. Directives coming
223-
// from the .d.ts already have them included through `ngAcceptInputType` class members.
224+
// from the .d.ts already have them included through `ngAcceptInputType` class members,
225+
// or via the `InputSignal` type of the member.
224226
transform: null,
225227
};
226228
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ export function extractDirectiveTypeCheckMeta(
124124
staticMembers.map(extractCoercedInput).filter((inputName): inputName is ClassPropertyName => {
125125
// If the input refers to a signal input, we will not respect coercion members.
126126
// A transform function should be used instead.
127-
// TODO(signals): consider throwing a diagnostic?
128127
if (inputName === null || inputs.getByClassPropertyName(inputName)?.isSignal) {
129128
return false;
130129
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,15 @@ function constructTypeCtorParameter(
143143
plainKeys.push(
144144
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(classPropertyName)));
145145
} else {
146+
const coercionType = transform != null ?
147+
transform.type.node :
148+
tsCreateTypeQueryForCoercedInput(rawType.typeName, classPropertyName);
149+
146150
coercedKeys.push(ts.factory.createPropertySignature(
147151
/* modifiers */ undefined,
148152
/* name */ classPropertyName,
149153
/* questionToken */ undefined,
150-
/* type */
151-
transform == null ?
152-
tsCreateTypeQueryForCoercedInput(rawType.typeName, classPropertyName) :
153-
transform.type.node));
154+
/* type */ coercionType));
154155
}
155156
}
156157
if (plainKeys.length > 0) {

packages/compiler-cli/src/ngtsc/typecheck/test/input_signal_diagnostics_spec.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ runInEachFileSystem(() => {
1717
id: string,
1818
inputs: Record<string, {type: string, isSignal: boolean, restrictionModifier?: string}>,
1919
template: string,
20+
extraDirectiveMembers?: string[],
2021
component?: string, expected: (string|jasmine.AsymmetricMatcher<string>)[],
2122
options?: Partial<TypeCheckingConfig>,
2223
focus?: boolean,
@@ -223,7 +224,23 @@ runInEachFileSystem(() => {
223224
honorAccessModifiersForInputBindings: false,
224225
},
225226
},
226-
// coercion not supported
227+
// coercion is not supported / respected
228+
{
229+
id: 'coercion members are not respected',
230+
inputs: {
231+
pattern: {
232+
type: 'InputSignal<string, string>',
233+
isSignal: true,
234+
},
235+
},
236+
extraDirectiveMembers: [
237+
'static ngAcceptInputType_pattern: string|boolean',
238+
],
239+
template: `<div dir [pattern]="false">`,
240+
expected: [
241+
`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`,
242+
],
243+
},
227244
// transforms
228245
{
229246
id: 'signal inputs write transform type respected',
@@ -249,6 +266,7 @@ runInEachFileSystem(() => {
249266
250267
class Dir {
251268
${inputFields.join('\n')}
269+
${c.extraDirectiveMembers?.join('\n') ?? ''}
252270
}
253271
class TestComponent {
254272
${c.component ?? ''}

packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError, LogicalFileSystem} f
1313
import {TestFile} from '../../file_system/testing';
1414
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, Reference, ReferenceEmitter, RelativePathStrategy} from '../../imports';
1515
import {NOOP_INCREMENTAL_BUILD} from '../../incremental';
16-
import {ClassPropertyMapping, CompoundMetadataReader, DirectiveMeta, HostDirectivesResolver, InputMapping, InputTransform, MatchSource, MetadataReaderWithIndex, MetaKind, NgModuleIndex} from '../../metadata';
16+
import {ClassPropertyMapping, CompoundMetadataReader, DecoratorInputTransform, DirectiveMeta, HostDirectivesResolver, InputMapping, MatchSource, MetadataReaderWithIndex, MetaKind, NgModuleIndex} from '../../metadata';
1717
import {NOOP_PERF_RECORDER} from '../../perf';
1818
import {TsCreateProgramDriver} from '../../program_driver';
1919
import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
@@ -249,7 +249,7 @@ export interface TestDirective extends Partial<Pick<
249249
bindingPropertyName: string;
250250
required: boolean;
251251
isSignal: boolean;
252-
transform: InputTransform|null;
252+
transform: DecoratorInputTransform|null;
253253
}
254254
};
255255
outputs?: {[fieldName: string]: string};

packages/compiler/src/render3/view/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,12 @@ export interface R3InputMetadata {
320320
bindingPropertyName: string;
321321
required: boolean;
322322
isSignal: boolean;
323+
/**
324+
* Transform function for the input.
325+
*
326+
* Null if there is no transform, or if this is a signal input.
327+
* Signal inputs capture their transform as part of the `InputSignal`.
328+
*/
323329
transformFunction: o.Expression|null;
324330
}
325331

packages/core/src/render3/interfaces/definition.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ export interface DirectiveDef<T> {
115115
* A dictionary mapping the private names of inputs to their transformation functions.
116116
* Note: the private names are used for the keys, rather than the public ones, because public
117117
* names can be re-aliased in host directives which would invalidate the lookup.
118+
*
119+
* Note: Signal inputs will not have transforms captured here. This is because their
120+
* transform function is already integrated into the `InputSignal`.
118121
*/
119122
readonly inputTransforms: {[classPropertyName: string]: InputTransformFunction}|null;
120123

0 commit comments

Comments
 (0)