Add migration support for the `@required` annotation.
If a named parameter is annotated as `@required`, then this overrides
the NamedNoDefaultParameterHeuristic; the parameter is considered
required regardless of what happens at call sites. A duplicate
`@required` annotation is not inserted.
Change-Id: Ib1385d0a65dd9001bb7abede9de2a319f65a1f86
Reviewed-on: https://dart-review.googlesource.com/c/92844
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Dan Rubel <danrubel@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/analysis_server/lib/src/nullability/constraint_gatherer.dart b/pkg/analysis_server/lib/src/nullability/constraint_gatherer.dart
index 57d05a6..e1e0f34 100644
--- a/pkg/analysis_server/lib/src/nullability/constraint_gatherer.dart
+++ b/pkg/analysis_server/lib/src/nullability/constraint_gatherer.dart
@@ -189,7 +189,10 @@
DecoratedType visitDefaultFormalParameter(DefaultFormalParameter node) {
var defaultValue = node.defaultValue;
if (defaultValue == null) {
- if (node.declaredElement.isOptionalPositional ||
+ if (node.declaredElement.hasRequired) {
+ // Nothing to do; the implicit default value of `null` will never be
+ // reached.
+ } else if (node.declaredElement.isOptionalPositional ||
assumptions.namedNoDefaultParameterHeuristic ==
NamedNoDefaultParameterHeuristic.assumeNullable) {
_recordFact(getOrComputeElementType(node.declaredElement).nullable);
diff --git a/pkg/analysis_server/lib/src/nullability/constraint_variable_gatherer.dart b/pkg/analysis_server/lib/src/nullability/constraint_variable_gatherer.dart
index 08db78d..1715526 100644
--- a/pkg/analysis_server/lib/src/nullability/constraint_variable_gatherer.dart
+++ b/pkg/analysis_server/lib/src/nullability/constraint_variable_gatherer.dart
@@ -58,14 +58,19 @@
DecoratedType visitDefaultFormalParameter(DefaultFormalParameter node) {
var decoratedType = node.parameter.accept(this);
ConstraintVariable optional;
- if (node.defaultValue != null) {
+ if (node.declaredElement.hasRequired) {
+ optional = null;
+ } else if (node.defaultValue != null) {
optional = ConstraintVariable.always;
} else {
optional = decoratedType.nullable;
_variables.recordPossiblyOptional(_source, node, optional);
}
- _currentFunctionType
- .namedParameterOptionalVariables[node.declaredElement.name] = optional;
+ if (optional != null) {
+ _currentFunctionType
+ .namedParameterOptionalVariables[node.declaredElement.name] =
+ optional;
+ }
return null;
}
diff --git a/pkg/analysis_server/test/src/nullability/migration_visitor_test.dart b/pkg/analysis_server/test/src/nullability/migration_visitor_test.dart
index ddfc799..12c9333 100644
--- a/pkg/analysis_server/test/src/nullability/migration_visitor_test.dart
+++ b/pkg/analysis_server/test/src/nullability/migration_visitor_test.dart
@@ -265,6 +265,32 @@
assertNoConstraints(decoratedTypeAnnotation('int').nullable);
}
+ test_functionDeclaration_parameter_named_no_default_required_assume_nullable() async {
+ addMetaPackage();
+ await analyze('''
+import 'package:meta/meta.dart';
+void f({@required int i}) {}
+''',
+ assumptions: NullabilityMigrationAssumptions(
+ namedNoDefaultParameterHeuristic:
+ NamedNoDefaultParameterHeuristic.assumeNullable));
+
+ assertNoConstraints(decoratedTypeAnnotation('int').nullable);
+ }
+
+ test_functionDeclaration_parameter_named_no_default_required_assume_required() async {
+ addMetaPackage();
+ await analyze('''
+import 'package:meta/meta.dart';
+void f({@required int i}) {}
+''',
+ assumptions: NullabilityMigrationAssumptions(
+ namedNoDefaultParameterHeuristic:
+ NamedNoDefaultParameterHeuristic.assumeRequired));
+
+ assertNoConstraints(decoratedTypeAnnotation('int').nullable);
+ }
+
test_functionDeclaration_parameter_positionalOptional_default_notNull() async {
await analyze('''
void f([int i = 1]) {}
@@ -327,7 +353,7 @@
assertConstraint([nullable_j], nullable_i);
}
- test_functionInvocation_parameter_named_optional() async {
+ test_functionInvocation_parameter_named_missing() async {
await analyze('''
void f({int i}) {}
void g() {
@@ -338,6 +364,23 @@
assertConstraint([], optional_i);
}
+ test_functionInvocation_parameter_named_missing_required() async {
+ addMetaPackage();
+ verifyNoTestUnitErrors = false;
+ await analyze('''
+import 'package:meta/meta.dart';
+void f({@required int i}) {}
+void g() {
+ f();
+}
+''');
+ // The call at `f()` is presumed to be in error; no constraint is recorded.
+ var optional_i = possiblyOptionalParameter('int i');
+ expect(optional_i, isNull);
+ var nullable_i = decoratedTypeAnnotation('int i').nullable;
+ assertNoConstraints(nullable_i);
+ }
+
test_functionInvocation_parameter_null() async {
await analyze('''
void f(int i) {}
@@ -638,6 +681,20 @@
same(decoratedType.nullable));
}
+ test_topLevelFunction_parameterType_named_no_default_required() async {
+ addMetaPackage();
+ await analyze('''
+import 'package:meta/meta.dart';
+void f({@required String s}) {}
+''');
+ var decoratedType = decoratedTypeAnnotation('String');
+ var functionType = decoratedFunctionType('f');
+ expect(functionType.namedParameters['s'], same(decoratedType));
+ expect(decoratedType.nullable, isNotNull);
+ expect(decoratedType.nullable, isNot(same(ConstraintVariable.always)));
+ expect(functionType.namedParameterOptionalVariables['s'], isNull);
+ }
+
test_topLevelFunction_parameterType_named_with_default() async {
await analyze('''
void f({String s: 'x'}) {}
diff --git a/pkg/analysis_server/test/src/nullability/provisional_api_test.dart b/pkg/analysis_server/test/src/nullability/provisional_api_test.dart
index ba2915f..8f9f86e 100644
--- a/pkg/analysis_server/test/src/nullability/provisional_api_test.dart
+++ b/pkg/analysis_server/test/src/nullability/provisional_api_test.dart
@@ -274,6 +274,55 @@
NamedNoDefaultParameterHeuristic.assumeRequired));
}
+ test_named_parameter_no_default_unused_required_option2_assume_nullable() async {
+ // The `@required` annotation overrides the assumption of nullability.
+ // The call at `f()` is presumed to be in error.
+ addMetaPackage();
+ var content = '''
+import 'package:meta/meta.dart';
+void f({@required String s}) {}
+main() {
+ f();
+}
+''';
+ var expected = '''
+import 'package:meta/meta.dart';
+void f({@required String s}) {}
+main() {
+ f();
+}
+''';
+ await _checkSingleFileChanges(content, expected,
+ assumptions: NullabilityMigrationAssumptions(
+ namedNoDefaultParameterHeuristic:
+ NamedNoDefaultParameterHeuristic.assumeNullable));
+ }
+
+ test_named_parameter_no_default_unused_required_option2_assume_required() async {
+ // Since the `@required` annotation is already present, it is not added
+ // again.
+ // The call at `f()` is presumed to be in error.
+ addMetaPackage();
+ var content = '''
+import 'package:meta/meta.dart';
+void f({@required String s}) {}
+main() {
+ f();
+}
+''';
+ var expected = '''
+import 'package:meta/meta.dart';
+void f({@required String s}) {}
+main() {
+ f();
+}
+''';
+ await _checkSingleFileChanges(content, expected,
+ assumptions: NullabilityMigrationAssumptions(
+ namedNoDefaultParameterHeuristic:
+ NamedNoDefaultParameterHeuristic.assumeRequired));
+ }
+
test_named_parameter_no_default_used_non_null_option2_assume_nullable() async {
var content = '''
void f({String s}) {}
@@ -312,6 +361,30 @@
NamedNoDefaultParameterHeuristic.assumeRequired));
}
+ test_named_parameter_no_default_used_non_null_required_option2_assume_required() async {
+ // Even if we are using the "assumeRequired" heuristic, we should not add a
+ // duplicate `@required` annotation.
+ addMetaPackage();
+ var content = '''
+import 'package:meta/meta.dart';
+void f({@required String s}) {}
+main() {
+ f(s: 'x');
+}
+''';
+ var expected = '''
+import 'package:meta/meta.dart';
+void f({@required String s}) {}
+main() {
+ f(s: 'x');
+}
+''';
+ await _checkSingleFileChanges(content, expected,
+ assumptions: NullabilityMigrationAssumptions(
+ namedNoDefaultParameterHeuristic:
+ NamedNoDefaultParameterHeuristic.assumeRequired));
+ }
+
test_named_parameter_no_default_used_null_option2() async {
var content = '''
void f({String s}) {}
@@ -328,6 +401,54 @@
await _checkSingleFileChanges(content, expected);
}
+ test_named_parameter_no_default_used_null_required_option2_assume_nullable() async {
+ // Explicitly passing `null` forces the parameter to be nullable even though
+ // it is required.
+ addMetaPackage();
+ var content = '''
+import 'package:meta/meta.dart';
+void f({@required String s}) {}
+main() {
+ f(s: null);
+}
+''';
+ var expected = '''
+import 'package:meta/meta.dart';
+void f({@required String? s}) {}
+main() {
+ f(s: null);
+}
+''';
+ await _checkSingleFileChanges(content, expected,
+ assumptions: NullabilityMigrationAssumptions(
+ namedNoDefaultParameterHeuristic:
+ NamedNoDefaultParameterHeuristic.assumeNullable));
+ }
+
+ test_named_parameter_no_default_used_null_required_option2_assume_required() async {
+ // Explicitly passing `null` forces the parameter to be nullable even though
+ // it is required.
+ addMetaPackage();
+ var content = '''
+import 'package:meta/meta.dart';
+void f({@required String s}) {}
+main() {
+ f(s: null);
+}
+''';
+ var expected = '''
+import 'package:meta/meta.dart';
+void f({@required String? s}) {}
+main() {
+ f(s: null);
+}
+''';
+ await _checkSingleFileChanges(content, expected,
+ assumptions: NullabilityMigrationAssumptions(
+ namedNoDefaultParameterHeuristic:
+ NamedNoDefaultParameterHeuristic.assumeRequired));
+ }
+
test_named_parameter_with_non_null_default_unused_option2() async {
var content = '''
void f({String s: 'foo'}) {}