From 63d3362bd4cfc3ab7a0fc978e38f0c25739f1343 Mon Sep 17 00:00:00 2001 From: Ryan Russell Date: Fri, 18 Apr 2025 16:50:53 +0000 Subject: [PATCH] fix(core): inject migration should treat @Attribute as optional The @Attribute decorator will inject null if a host attribute is missing, but `inject(new HostAttributeToken(...))` will throw a no provider error. We should set {optional: true} when migrating an @Attribute decorator. Also allow nonNullableOptional to add `!` to those declarations. --- .../ng-generate/inject-migration/migration.ts | 23 +++++++++++----- .../schematics/test/inject_migration_spec.ts | 27 ++++++++++++++++++- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/packages/core/schematics/ng-generate/inject-migration/migration.ts b/packages/core/schematics/ng-generate/inject-migration/migration.ts index 733325740662..5a268bf31a01 100644 --- a/packages/core/schematics/ng-generate/inject-migration/migration.ts +++ b/packages/core/schematics/ng-generate/inject-migration/migration.ts @@ -409,7 +409,7 @@ function createInjectReplacementCall( const moduleName = '@angular/core'; const sourceFile = param.getSourceFile(); const decorators = getAngularDecorators(localTypeChecker, ts.getDecorators(param) || []); - const literalProps: ts.ObjectLiteralElementLike[] = []; + const literalProps = new Set(); const type = param.type; let injectedType = ''; let typeArguments = type && hasGenerics(type) ? [type] : undefined; @@ -451,24 +451,27 @@ function createInjectReplacementCall( const expression = ts.factory.createNewExpression(constructorRef, undefined, [firstArg]); injectedType = printer.printNode(ts.EmitHint.Unspecified, expression, sourceFile); typeArguments = undefined; + // @Attribute is implicitly optional. + hasOptionalDecorator = true; + literalProps.add('optional'); } break; case 'Optional': hasOptionalDecorator = true; - literalProps.push(ts.factory.createPropertyAssignment('optional', ts.factory.createTrue())); + literalProps.add('optional'); break; case 'SkipSelf': - literalProps.push(ts.factory.createPropertyAssignment('skipSelf', ts.factory.createTrue())); + literalProps.add('skipSelf'); break; case 'Self': - literalProps.push(ts.factory.createPropertyAssignment('self', ts.factory.createTrue())); + literalProps.add('self'); break; case 'Host': - literalProps.push(ts.factory.createPropertyAssignment('host', ts.factory.createTrue())); + literalProps.add('host'); break; } } @@ -479,8 +482,14 @@ function createInjectReplacementCall( const injectRef = tracker.addImport(param.getSourceFile(), 'inject', moduleName); const args: ts.Expression[] = [ts.factory.createIdentifier(PLACEHOLDER)]; - if (literalProps.length > 0) { - args.push(ts.factory.createObjectLiteralExpression(literalProps)); + if (literalProps.size > 0) { + args.push( + ts.factory.createObjectLiteralExpression( + Array.from(literalProps, (prop) => + ts.factory.createPropertyAssignment(prop, ts.factory.createTrue()), + ), + ), + ); } let expression: ts.Expression = ts.factory.createCallExpression(injectRef, typeArguments, args); diff --git a/packages/core/schematics/test/inject_migration_spec.ts b/packages/core/schematics/test/inject_migration_spec.ts index c80f8e2041c8..bb12619d11ce 100644 --- a/packages/core/schematics/test/inject_migration_spec.ts +++ b/packages/core/schematics/test/inject_migration_spec.ts @@ -194,7 +194,7 @@ describe('inject migration', () => { ``, `@Directive()`, `class MyDir {`, - ` private foo = inject(new HostAttributeToken('foo'));`, + ` private foo = inject(new HostAttributeToken('foo'), { optional: true });`, `}`, ]); }); @@ -1256,6 +1256,31 @@ describe('inject migration', () => { ]); }); + it('should add non-null assertion for @Attribute injections when enabled', async () => { + writeFile( + '/dir.ts', + [ + `import { Attribute, Directive } from '@angular/core';`, + ``, + `@Directive()`, + `class MyDir {`, + ` constructor(@Attribute('tabindex') private foo: string) {}`, + `}`, + ].join('\n'), + ); + + await runMigration({nonNullableOptional: true}); + + expect(tree.readContent('/dir.ts').split('\n')).toEqual([ + `import { Directive, HostAttributeToken, inject } from '@angular/core';`, + ``, + `@Directive()`, + `class MyDir {`, + ` private foo = inject(new HostAttributeToken('tabindex'), { optional: true })!;`, + `}`, + ]); + }); + it('should pick up the first non-literal type if a parameter has a union type', async () => { writeFile( '/dir.ts',