Skip to content

Commit 304d359

Browse files
committed
refactor(compiler): add support for @input transforms under isolatedDeclarations
Adds support for `@Input` transform functions in isolated declarations mode (`emitDeclarationOnly: true`), allowing components and directives to specify `transform` functions without triggering fatal compiler errors. Synthesizes the `ngAcceptInputType_` write type syntactically: - For referenced functions (`transform: booleanAttribute`), emits `Parameters<typeof booleanAttribute>[0]`, relying on downstream template type checking to resolve the type. - For inline functions (`transform: (v: string) => boolean`), extracts `parameters[0].type` directly from the local TypeScript AST.
1 parent bee4eff commit 304d359

4 files changed

Lines changed: 127 additions & 54 deletions

File tree

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {ClassPropertyMapping, outputAst} from '@angular/compiler';
1010

1111
import {InputMapping} from '../../../metadata';
12+
import {Reference} from '../../../imports';
1213
import {CompileResult} from '../../../transform';
1314

1415
/** Generates additional fields to be added to a class that has inputs with transform functions. */
@@ -20,11 +21,12 @@ export function compileInputTransformFields(
2021
for (const input of inputs) {
2122
// Note: Signal inputs capture their transform `WriteT` as part of the `InputSignal`.
2223
// Such inputs will not have a `transform` captured and not generate coercion members.
23-
2424
if (input.transform) {
2525
extraFields.push({
2626
name: `ngAcceptInputType_${input.classPropertyName}`,
27-
type: outputAst.transplantedType(input.transform.type),
27+
type: input.transform.type.synthetic
28+
? new outputAst.ExpressionType(new outputAst.WrappedNodeExpr(input.transform.type.node))
29+
: outputAst.transplantedType(input.transform.type),
2830
statements: [],
2931
initializer: null,
3032
deferrableImports: null,

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

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
createMayBeForwardRefExpression,
1313
emitDistinctChangesOnlyDefaultValue,
1414
Expression,
15+
ExpressionType,
1516
ExternalExpr,
1617
ExternalReference,
1718
ForwardRefHandling,
@@ -1502,20 +1503,68 @@ export function parseDecoratorInputTransformFunction(
15021503
emitDeclarationOnly: boolean,
15031504
): DecoratorInputTransform {
15041505
if (emitDeclarationOnly) {
1505-
const chain: ts.DiagnosticMessageChain = {
1506-
messageText:
1507-
'@Input decorators with a transform function are not supported in experimental declaration-only emission mode',
1508-
category: ts.DiagnosticCategory.Error,
1509-
code: 0,
1510-
next: [
1511-
{
1512-
messageText: `Consider converting '${clazz.name.text}.${classPropertyName}' to an input signal`,
1513-
category: ts.DiagnosticCategory.Message,
1514-
code: 0,
1515-
},
1516-
],
1517-
};
1518-
throw new FatalDiagnosticError(ErrorCode.DECORATOR_UNEXPECTED, value.node, chain);
1506+
if (ts.isArrowFunction(value.node) || ts.isFunctionExpression(value.node)) {
1507+
const firstParam =
1508+
value.node.parameters[0]?.name?.getText() === 'this'
1509+
? value.node.parameters[1]
1510+
: value.node.parameters[0];
1511+
1512+
if (!firstParam) {
1513+
const ref = new Reference(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword));
1514+
ref.synthetic = true;
1515+
return {
1516+
node: value.node,
1517+
type: ref,
1518+
};
1519+
}
1520+
1521+
if (!firstParam.type) {
1522+
throw createValueHasWrongTypeError(
1523+
value.node,
1524+
value,
1525+
'Input transform function first parameter must have a type',
1526+
);
1527+
}
1528+
1529+
if (firstParam.dotDotDotToken) {
1530+
throw createValueHasWrongTypeError(
1531+
value.node,
1532+
value,
1533+
'Input transform function first parameter cannot be a spread parameter',
1534+
);
1535+
}
1536+
1537+
const ref = new Reference(firstParam.type);
1538+
ref.synthetic = true;
1539+
return {
1540+
node: value.node,
1541+
type: ref,
1542+
};
1543+
}
1544+
1545+
const node =
1546+
value instanceof Reference ? value.getIdentityIn(clazz.getSourceFile()) : value.node;
1547+
const entityName = node ? expressionToEntityName(node as ts.Expression) : null;
1548+
if (entityName !== null) {
1549+
const typeQuery = ts.factory.createTypeQueryNode(entityName);
1550+
const parametersType = ts.factory.createTypeReferenceNode('Parameters', [typeQuery]);
1551+
const indexedAccess = ts.factory.createIndexedAccessTypeNode(
1552+
parametersType,
1553+
ts.factory.createLiteralTypeNode(ts.factory.createNumericLiteral('0')),
1554+
);
1555+
const ref = new Reference(indexedAccess);
1556+
ref.synthetic = true;
1557+
return {
1558+
node: value.node,
1559+
type: ref,
1560+
};
1561+
}
1562+
1563+
throw createValueHasWrongTypeError(
1564+
value.node,
1565+
value,
1566+
'Input transform function could not be referenced',
1567+
);
15191568
}
15201569
// In local compilation mode we can skip type checking the function args. This is because usually
15211570
// the type check is done in a separate build which runs in full compilation mode. So here we skip
@@ -2208,3 +2257,14 @@ export function extractHostBindingResources(nodes: HostBindingNodes): ReadonlySe
22082257

22092258
return result;
22102259
}
2260+
2261+
function expressionToEntityName(expr: ts.Expression): ts.EntityName | null {
2262+
if (ts.isIdentifier(expr)) {
2263+
return expr;
2264+
}
2265+
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
2266+
const left = expressionToEntityName(expr.expression);
2267+
return left === null ? null : ts.factory.createQualifiedName(left, expr.name);
2268+
}
2269+
return null;
2270+
}

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_directives/host_directives/TEST_CASES.json

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,7 @@
1515
]
1616
}
1717
],
18-
"compilationModeFilter": [
19-
"full compile",
20-
"linked compile",
21-
"local compile",
22-
"declaration-only emit"
23-
]
18+
"compilationModeFilter": ["full compile", "linked compile", "declaration-only emit"]
2419
},
2520
{
2621
"description": "should create hostDirective definitions for a chain of host directives",
@@ -36,12 +31,7 @@
3631
]
3732
}
3833
],
39-
"compilationModeFilter": [
40-
"full compile",
41-
"linked compile",
42-
"local compile",
43-
"declaration-only emit"
44-
]
34+
"compilationModeFilter": ["full compile", "linked compile", "declaration-only emit"]
4535
},
4636
{
4737
"description": "should handle a forwardRef used in hostDirectives",
@@ -57,12 +47,7 @@
5747
]
5848
}
5949
],
60-
"compilationModeFilter": [
61-
"full compile",
62-
"linked compile",
63-
"local compile",
64-
"declaration-only emit"
65-
]
50+
"compilationModeFilter": ["full compile", "linked compile", "declaration-only emit"]
6651
},
6752
{
6853
"description": "should handle the `inputs` and `outputs` options in host directives",
@@ -78,12 +63,7 @@
7863
]
7964
}
8065
],
81-
"compilationModeFilter": [
82-
"full compile",
83-
"linked compile",
84-
"local compile",
85-
"declaration-only emit"
86-
]
66+
"compilationModeFilter": ["full compile", "linked compile", "declaration-only emit"]
8767
},
8868
{
8969
"description": "should handle aliases to aliased `inputs` and `outputs` of a host directive",
@@ -99,12 +79,7 @@
9979
]
10080
}
10181
],
102-
"compilationModeFilter": [
103-
"full compile",
104-
"linked compile",
105-
"local compile",
106-
"declaration-only emit"
107-
]
82+
"compilationModeFilter": ["full compile", "linked compile", "declaration-only emit"]
10883
}
10984
]
11085
}

packages/compiler-cli/test/ngtsc/declaration_only_emission_spec.ts

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,7 @@ runInEachFileSystem(() => {
827827
);
828828
});
829829

830-
it('should show correct error message when using an @Input decorator with a transform function', () => {
830+
it('should emit type declarations when using an @Input decorator with a transform function', () => {
831831
env.write(
832832
'test.ts',
833833
`
@@ -840,16 +840,52 @@ runInEachFileSystem(() => {
840840
`,
841841
);
842842

843+
env.driveMain();
844+
const dtsContent = env.getContents('test.d.ts');
845+
846+
expect(dtsContent).toContain(
847+
'static ngAcceptInputType_decoratedInput: Parameters<typeof i0.booleanAttribute>[0];',
848+
);
849+
});
850+
851+
it('should emit type declarations when using an @Input decorator with an inline transform function', () => {
852+
env.write(
853+
'test.ts',
854+
`
855+
import {Component, Input} from '@angular/core';
856+
857+
@Component({template: '', selector: 'comp'})
858+
export class Comp {
859+
@Input({ transform: (v: string) => !!v }) decoratedInput!: boolean;
860+
}
861+
`,
862+
);
863+
864+
env.driveMain();
865+
const dtsContent = env.getContents('test.d.ts');
866+
867+
expect(dtsContent).toContain('static ngAcceptInputType_decoratedInput: string;');
868+
});
869+
870+
it('should produce a diagnostic when using an @Input decorator with an inline transform function missing a parameter type', () => {
871+
env.write(
872+
'test.ts',
873+
`
874+
import {Component, Input} from '@angular/core';
875+
876+
@Component({template: '', selector: 'comp'})
877+
export class Comp {
878+
@Input({ transform: (v) => !!v }) decoratedInput!: boolean;
879+
}
880+
`,
881+
);
882+
843883
const errors = env.driveDiagnostics();
844884

845885
expect(errors.length).toBe(1);
846-
expect(errors[0].code).toBe(ngErrorCode(ErrorCode.DECORATOR_UNEXPECTED));
847-
const errorMessage = ts.flattenDiagnosticMessageText(errors[0].messageText, '\n');
848-
expect(errorMessage).toContain(
849-
'@Input decorators with a transform function are not supported in experimental declaration-only emission mode',
850-
);
851-
expect(errorMessage).toContain(
852-
`Consider converting 'Comp.decoratedInput' to an input signal`,
886+
expect(errors[0].code).toBe(ngErrorCode(ErrorCode.VALUE_HAS_WRONG_TYPE));
887+
expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toContain(
888+
'Input transform function first parameter must have a type',
853889
);
854890
});
855891

0 commit comments

Comments
 (0)