Skip to content

Commit 9378d5c

Browse files
Ilya YanokCommit Queue
Ilya Yanok
authored and
Commit Queue
committed
Add hints for Angular constructor arguments
The rules are: 1. Arguments annotated with `@Optional` must be nullable (that part worked before this change). 2. Arguments annotated with `@Attribute` must be nullable if it's a constructor of a component (`@Attribute` can also appear on arguments of directives constructors, it doesn't provide any signal in the latter case). 3. Arguments of constructors of components or `@Injectable` classes, that have neither `@Optional` nor `@Attribute`, must be non-nullable. Tested: Added new test cases to `api_test.dart` Change-Id: I0f06a128e0f6ec49d39cc6bf025fe1796938f1f1 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/272900 Reviewed-by: Paul Berry <[email protected]> Commit-Queue: Ilya Yanok <[email protected]>
1 parent c704e11 commit 9378d5c

File tree

5 files changed

+155
-6
lines changed

5 files changed

+155
-6
lines changed

pkg/nnbd_migration/lib/instrumentation.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ enum EdgeOriginKind {
251251
alreadyMigratedType,
252252
alwaysNullableType,
253253
angularAnnotation,
254+
angularConstructorArgument,
254255
argumentErrorCheckNotNull,
255256
assignmentFromAngularInjectorGet,
256257
builtValueNullableAnnotation,

pkg/nnbd_migration/lib/src/edge_origin.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class AlwaysNullableTypeOrigin extends EdgeOrigin {
6464
}
6565

6666
/// Edge origin resulting from the presence of an Angular annotation such as
67-
/// `@Optional()`, `@ViewChild(...)`, or `@ContentChild(...)`.
67+
/// `@Optional()`, `@Attribute(...)`, `@ViewChild(...)`, or `@ContentChild(...)`.
6868
class AngularAnnotationOrigin extends EdgeOrigin {
6969
AngularAnnotationOrigin(super.source, AstNode super.node);
7070

@@ -76,6 +76,21 @@ class AngularAnnotationOrigin extends EdgeOrigin {
7676
EdgeOriginKind get kind => EdgeOriginKind.angularAnnotation;
7777
}
7878

79+
/// Edge origin resulting from `@Component(...)` or `@Injectable()` class
80+
/// constructor arguments having neither `@Optional()` nor `@Attribute()`
81+
/// annotation.
82+
class AngularConstructorArgumentOrigin extends EdgeOrigin {
83+
AngularConstructorArgumentOrigin(super.source, AstNode super.node);
84+
85+
@override
86+
String get description =>
87+
'Component or @Injectable() class constructor arguments without'
88+
' @Optional/@Attribute annotations must be non-nullable';
89+
90+
@override
91+
EdgeOriginKind get kind => EdgeOriginKind.angularConstructorArgument;
92+
}
93+
7994
/// Edge origin resulting from the presence of a call to
8095
/// `ArgumentError.checkNotNull`.
8196
///

pkg/nnbd_migration/lib/src/node_builder.dart

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ class NodeBuilder extends GeneralizingAstVisitor<DecoratedType>
5757
/// Otherwise `null`.
5858
NullabilityNodeTarget? _target;
5959

60+
/// [ClassDeclaration] for the current class or `null` if we are currently
61+
/// not inside a class declaration.
62+
ClassDeclaration? _classDeclaration;
63+
6064
final NullabilityMigrationListener? listener;
6165

6266
final NullabilityMigrationInstrumentation? instrumentation;
@@ -80,6 +84,16 @@ class NodeBuilder extends GeneralizingAstVisitor<DecoratedType>
8084
return NullabilityNodeTarget.text('unknown');
8185
}
8286

87+
bool get _isInjectable =>
88+
_classDeclaration?.metadata
89+
.any((ann) => _isAngularConstructor(ann.element, 'Injectable')) ??
90+
false;
91+
bool get _isInsideAngularComponent =>
92+
_classDeclaration?.metadata
93+
.toList()
94+
.any((ann) => _isAngularConstructor(ann.element, 'Component')) ??
95+
false;
96+
8397
@override
8498
DecoratedType? visitAsExpression(AsExpression node) {
8599
node.expression.accept(this);
@@ -130,7 +144,9 @@ class NodeBuilder extends GeneralizingAstVisitor<DecoratedType>
130144
node.metadata.accept(this);
131145
node.typeParameters?.accept(this);
132146
node.nativeClause?.accept(this);
147+
_classDeclaration = node;
133148
node.members.accept(this);
149+
_classDeclaration = null;
134150
var classElement = node.declaredElement!;
135151
_handleSupertypeClauses(node, classElement, node.extendsClause?.superclass,
136152
node.withClause, node.implementsClause, null);
@@ -841,15 +857,26 @@ class NodeBuilder extends GeneralizingAstVisitor<DecoratedType>
841857
_handleNullabilityHint(node, decoratedType);
842858
}
843859
_variables.recordDecoratedElementType(declaredElement, decoratedType);
860+
var isAnnotated = false;
844861
for (var annotation in node.metadata) {
845862
var element = annotation.element;
846-
if (element is ConstructorElement &&
847-
element.enclosingElement.name == 'Optional' &&
848-
_isAngularUri(element.librarySource.uri)) {
849-
_graph.makeNullable(
850-
decoratedType!.node, AngularAnnotationOrigin(source, node));
863+
if (_isAngularConstructor(element, 'Optional') ||
864+
_isAngularConstructor(element, 'Attribute')) {
865+
isAnnotated = true;
866+
if (_isAngularConstructor(element, 'Optional') ||
867+
_isInsideAngularComponent) {
868+
_graph.makeNullable(
869+
decoratedType!.node, AngularAnnotationOrigin(source, node));
870+
}
851871
}
852872
}
873+
if (declaredElement.enclosingElement is ConstructorElement &&
874+
(_isInsideAngularComponent || _isInjectable) &&
875+
!isAnnotated) {
876+
_graph.makeNonNullable(
877+
decoratedType!.node, AngularConstructorArgumentOrigin(source, node));
878+
}
879+
853880
if (declaredElement.isNamed) {
854881
_namedParameters![declaredElement.name] = decoratedType!;
855882
} else {
@@ -938,6 +965,13 @@ class NodeBuilder extends GeneralizingAstVisitor<DecoratedType>
938965
declaredElement, decoratedSupertypes);
939966
}
940967

968+
/// Determines whether [element] is a constructor named [name] from Angular
969+
/// package.
970+
bool _isAngularConstructor(Element? element, String name) =>
971+
element is ConstructorElement &&
972+
element.enclosingElement.name == name &&
973+
_isAngularUri(element.librarySource.uri);
974+
941975
/// Determines whether the given [uri] comes from the Angular package.
942976
bool _isAngularUri(Uri uri) {
943977
if (!uri.isScheme('package')) return false;

pkg/nnbd_migration/test/abstract_context.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ class AbstractContextTest with ResourceProviderMixin {
5656
addPackageFile(
5757
internalUris ? 'third_party.dart_src.angular.angular' : 'angular',
5858
'angular.dart', '''
59+
class Component {
60+
const Component({
61+
required String selector,
62+
// optional arguments skipped
63+
})
64+
}
65+
class Attribute {
66+
const Attribute(String attributeName);
67+
}
5968
class ContentChild {
6069
const ContentChild(Object selector, {Object? read});
6170
}
@@ -74,6 +83,9 @@ class ViewChildren {
7483
class Injector {
7584
dynamic get(Object token, [Object? notFoundValue]) => null;
7685
}
86+
class Injectable {
87+
const Injectable();
88+
}
7789
''');
7890
if (internalUris) {
7991
addPackageFile('angular', 'angular.dart', '''

pkg/nnbd_migration/test/api_test.dart

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,66 @@ g() {
356356
await _checkSingleFileChanges(content, expected);
357357
}
358358

359+
Future<void> test_angular_component_attribute() async {
360+
addAngularPackage();
361+
var content = '''
362+
import 'dart:html';
363+
import 'package:angular/angular.dart';
364+
365+
@Component(
366+
selector: 'my-component'
367+
)
368+
class MyComponent {
369+
int foo;
370+
MyComponent(@Attribute('foo') this.foo);
371+
}
372+
''';
373+
var expected = '''
374+
import 'dart:html';
375+
import 'package:angular/angular.dart';
376+
377+
@Component(
378+
selector: 'my-component'
379+
)
380+
class MyComponent {
381+
int? foo;
382+
MyComponent(@Attribute('foo') this.foo);
383+
}
384+
''';
385+
await _checkSingleFileChanges(content, expected);
386+
}
387+
388+
Future<void> test_angular_component_constructor() async {
389+
addAngularPackage();
390+
var content = '''
391+
import 'dart:html';
392+
import 'package:angular/angular.dart';
393+
394+
@Component(
395+
selector: 'my-component'
396+
)
397+
class MyComponent {
398+
int foo;
399+
MyComponent(this.foo);
400+
void nullifyFoo() { foo = null; }
401+
}
402+
''';
403+
var expected = '''
404+
import 'dart:html';
405+
import 'package:angular/angular.dart';
406+
407+
@Component(
408+
selector: 'my-component'
409+
)
410+
class MyComponent {
411+
int? foo;
412+
MyComponent(int this.foo);
413+
void nullifyFoo() { foo = null; }
414+
}
415+
''';
416+
await _checkSingleFileChanges(content, expected);
417+
}
418+
359419
Future<void> test_angular_contentChild_field() async {
360420
addAngularPackage();
361421
var content = '''
@@ -469,6 +529,33 @@ class myComponent {
469529
await _checkSingleFileChanges(content, expected);
470530
}
471531

532+
Future<void> test_angular_injectable_constructor() async {
533+
addAngularPackage();
534+
var content = '''
535+
import 'dart:html';
536+
import 'package:angular/angular.dart';
537+
538+
@Injectable()
539+
class MyClass {
540+
int foo;
541+
MyClass(this.foo);
542+
void nullifyFoo() { foo = null; }
543+
}
544+
''';
545+
var expected = '''
546+
import 'dart:html';
547+
import 'package:angular/angular.dart';
548+
549+
@Injectable()
550+
class MyClass {
551+
int? foo;
552+
MyClass(int this.foo);
553+
void nullifyFoo() { foo = null; }
554+
}
555+
''';
556+
await _checkSingleFileChanges(content, expected);
557+
}
558+
472559
Future<void> test_angular_optional_constructor_param() async {
473560
addAngularPackage();
474561
var content = '''

0 commit comments

Comments
 (0)