Migration: add support for Angular's @Optional() annotation.

Bug: https://github.com/dart-lang/sdk/issues/45661
Change-Id: I6c8c87c2a0d26dc8053ef66961a96e03cf5a2582
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/209780
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
This commit is contained in:
Paul Berry 2021-08-10 21:32:51 +00:00 committed by commit-bot@chromium.org
parent ae0880a5c0
commit 4d0dc2ea8a
5 changed files with 114 additions and 0 deletions

View file

@ -272,6 +272,7 @@ enum EdgeOriginKind {
nonNullableUsage,
nonNullAssertion,
nullabilityComment,
optionalAnnotation,
optionalFormalParameter,
parameterInheritance,
quiverCheckNotNull,

View file

@ -542,6 +542,18 @@ class NullabilityCommentOrigin extends EdgeOrigin {
EdgeOriginKind get kind => EdgeOriginKind.nullabilityComment;
}
/// Edge origin resulting from the presence of an Angular `@Optional()`
/// annotation.
class OptionalAnnotationOrigin extends EdgeOrigin {
OptionalAnnotationOrigin(Source? source, AstNode node) : super(source, node);
@override
String get description => "annotated with Angular's @Optional() annotation";
@override
EdgeOriginKind get kind => EdgeOriginKind.optionalAnnotation;
}
/// Edge origin resulting from the presence of an optional formal parameter.
///
/// For example, in the following code snippet:

View file

@ -794,6 +794,15 @@ class NodeBuilder extends GeneralizingAstVisitor<DecoratedType>
_handleNullabilityHint(node, decoratedType);
}
_variables!.recordDecoratedElementType(declaredElement, decoratedType);
for (var annotation in node.metadata) {
var element = annotation.element;
if (element is ConstructorElement &&
element.enclosingElement.name == 'Optional' &&
_isAngularUri(element.librarySource.uri)) {
_graph.makeNullable(
decoratedType!.node!, OptionalAnnotationOrigin(source, node));
}
}
if (declaredElement.isNamed) {
_namedParameters![declaredElement.name] = decoratedType;
} else {
@ -884,6 +893,18 @@ class NodeBuilder extends GeneralizingAstVisitor<DecoratedType>
.recordDecoratedDirectSupertypes(declaredElement, decoratedSupertypes);
}
/// Determines whether the given [uri] comes from the Angular package.
bool _isAngularUri(Uri uri) {
if (uri.scheme != 'package') return false;
var packageName = uri.pathSegments[0];
if (packageName == 'angular') return true;
if (packageName == 'third_party.dart_src.angular.angular') {
// This name is used for angular development internally at Google.
return true;
}
return false;
}
T _pushNullabilityNodeTarget<T>(
NullabilityNodeTarget target, T Function() fn) {
NullabilityNodeTarget? previousTarget = _target;

View file

@ -45,6 +45,27 @@ class AbstractContextTest with ResourceProviderMixin {
String get testsPath => '$homePath/tests';
/// Makes a mock version of the Angular package available for unit testing.
///
/// If optional argument [internalUris] is `true`, the mock Angular package
/// will be located in a package called `third_party.dart_src.angular.angular`
/// (as it is in Google3), and `package:angular` will simply re-export it;
/// this allows the test to reflect usage in internal sources.
void addAngularPackage({bool internalUris = false}) {
addPackageFile(
internalUris ? 'third_party.dart_src.angular.angular' : 'angular',
'angular.dart', '''
class Optional {
const Optional();
}
''');
if (internalUris) {
addPackageFile('angular', 'angular.dart', '''
export 'package:third_party.dart_src.angular.angular/angular.dart';
''');
}
}
void addBuiltValuePackage() {
addPackageFile('built_value', 'built_value.dart', '''
abstract class Built<V extends Built<V, B>, B extends Builder<V, B>> {}

View file

@ -356,6 +356,65 @@ g() {
await _checkSingleFileChanges(content, expected);
}
Future<void> test_angular_optional_constructor_param() async {
addAngularPackage();
var content = '''
import 'package:angular/angular.dart';
class MyComponent {
MyComponent(@Optional() String foo);
}
''';
var expected = '''
import 'package:angular/angular.dart';
class MyComponent {
MyComponent(@Optional() String? foo);
}
''';
await _checkSingleFileChanges(content, expected);
}
Future<void> test_angular_optional_constructor_param_field_formal() async {
addAngularPackage();
var content = '''
import 'package:angular/angular.dart';
class MyComponent {
String foo;
MyComponent(@Optional() this.foo);
}
''';
var expected = '''
import 'package:angular/angular.dart';
class MyComponent {
String? foo;
MyComponent(@Optional() this.foo);
}
''';
await _checkSingleFileChanges(content, expected);
}
Future<void> test_angular_optional_constructor_param_internal() async {
addAngularPackage(internalUris: true);
var content = '''
import 'package:angular/angular.dart';
class MyComponent {
MyComponent(@Optional() String foo);
}
''';
var expected = '''
import 'package:angular/angular.dart';
class MyComponent {
MyComponent(@Optional() String? foo);
}
''';
await _checkSingleFileChanges(content, expected);
}
Future<void> test_argumentError_checkNotNull_implies_non_null_intent() async {
var content = '''
void f(int i) {