Migrator: Add /*required*/ hints when meta not imported.
Currently the migrator can add `@required` from the preview, which will crash
the preview upon rerunning, if package:meta is not imported.
This change instead adds a /*required*/ hint when meta is not imported.
Additionally, /*required*/ is understood by the migrator just as if it were
`@required`.
Fixes https://github.com/dart-lang/sdk/issues/43751
Change-Id: I1ea532246b956feacedd179146372b13c65003df
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/169900
Commit-Queue: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Paul Berry <paulberry@google.com>
diff --git a/pkg/nnbd_migration/lib/src/edge_builder.dart b/pkg/nnbd_migration/lib/src/edge_builder.dart
index 282b622..d10f7f4 100644
--- a/pkg/nnbd_migration/lib/src/edge_builder.dart
+++ b/pkg/nnbd_migration/lib/src/edge_builder.dart
@@ -212,6 +212,8 @@
final Set<PromotableElement> _lateHintedLocals = {};
+ final Set<PromotableElement> _requiredHintedParameters = {};
+
final Map<Token, HintComment> _nullCheckHints = {};
/// Helper that assists us in transforming Iterable methods to their "OrNull"
@@ -745,6 +747,10 @@
if (node.declaredElement.hasRequired) {
// Nothing to do; the implicit default value of `null` will never be
// reached.
+ } else if (_variables.getRequiredHint(source, node) != null) {
+ // Nothing to do; assume the implicit default value of `null` will never
+ // be reached.
+ _requiredHintedParameters.add(node.declaredElement);
} else {
_graph.makeNullable(getOrComputeElementType(node.declaredElement).node,
OptionalFormalParameterOrigin(source, node));
@@ -1536,6 +1542,7 @@
if (!node.inDeclarationContext() &&
node.inGetterContext() &&
!_lateHintedLocals.contains(staticElement) &&
+ !_requiredHintedParameters.contains(staticElement) &&
!_flowAnalysis.isAssigned(staticElement)) {
_graph.makeNullable(type.node, UninitializedReadOrigin(source, node));
}
diff --git a/pkg/nnbd_migration/lib/src/front_end/info_builder.dart b/pkg/nnbd_migration/lib/src/front_end/info_builder.dart
index 2f9f0fb..b1d3f84 100644
--- a/pkg/nnbd_migration/lib/src/front_end/info_builder.dart
+++ b/pkg/nnbd_migration/lib/src/front_end/info_builder.dart
@@ -123,14 +123,16 @@
'Reason', [_makeTraceEntry(info.description, info.codeReference)]));
}
- /// Return an edit that can be applied.
+ /// Returns a list of edits that can be applied.
List<EditDetail> _computeEdits(
- AtomicEditInfo fixInfo, int offset, String content) {
- EditDetail _removeHint(String description) => EditDetail.fromSourceEdit(
+ AtomicEditInfo fixInfo, int offset, ResolvedUnitResult result) {
+ var content = result.content;
+
+ EditDetail removeHint(String description) => EditDetail.fromSourceEdit(
description,
fixInfo.hintComment.changesToRemove(content).toSourceEdits().single);
- EditDetail _changeHint(String description, String replacement) =>
+ EditDetail changeHint(String description, String replacement) =>
EditDetail.fromSourceEdit(
description,
fixInfo.hintComment
@@ -142,16 +144,27 @@
var fixKind = fixInfo.description.kind;
switch (fixKind) {
case NullabilityFixKind.addLateDueToHint:
- edits.add(_removeHint('Remove /*late*/ hint'));
+ edits.add(removeHint('Remove /*late*/ hint'));
break;
case NullabilityFixKind.addLateFinalDueToHint:
- edits.add(_removeHint('Remove /*late final*/ hint'));
+ edits.add(removeHint('Remove /*late final*/ hint'));
break;
case NullabilityFixKind.addRequired:
- // TODO(brianwilkerson) This doesn't verify that the meta package has
- // been imported.
- edits
- .add(EditDetail("Mark with '@required'.", offset, 0, '@required '));
+ var metaImport =
+ _findImportDirective(result.unit, 'package:meta/meta.dart');
+ if (metaImport == null) {
+ edits.add(
+ EditDetail('Add /*required*/ hint', offset, 0, '/*required*/ '));
+ } else {
+ var prefix = metaImport.prefix?.name;
+ if (prefix == null) {
+ edits.add(
+ EditDetail("Mark with '@required'", offset, 0, '@required '));
+ } else {
+ edits.add(EditDetail(
+ "Mark with '@required'", offset, 0, '@$prefix.required '));
+ }
+ }
break;
case NullabilityFixKind.checkExpression:
// TODO(brianwilkerson) Determine whether we can know that the fix is
@@ -159,7 +172,7 @@
edits.add(EditDetail('Add /*!*/ hint', offset, 0, '/*!*/'));
break;
case NullabilityFixKind.checkExpressionDueToHint:
- edits.add(_removeHint('Remove /*!*/ hint'));
+ edits.add(removeHint('Remove /*!*/ hint'));
break;
case NullabilityFixKind.downcastExpression:
case NullabilityFixKind.otherCastExpression:
@@ -180,12 +193,12 @@
edits.add(EditDetail('Add /*?*/ hint', offset, 0, '/*?*/'));
break;
case NullabilityFixKind.makeTypeNullableDueToHint:
- edits.add(_changeHint('Change to /*!*/ hint', '/*!*/'));
- edits.add(_removeHint('Remove /*?*/ hint'));
+ edits.add(changeHint('Change to /*!*/ hint', '/*!*/'));
+ edits.add(removeHint('Remove /*?*/ hint'));
break;
case NullabilityFixKind.typeNotMadeNullableDueToHint:
- edits.add(_removeHint('Remove /*!*/ hint'));
- edits.add(_changeHint('Change to /*?*/ hint', '/*?*/'));
+ edits.add(removeHint('Remove /*!*/ hint'));
+ edits.add(changeHint('Change to /*?*/ hint', '/*?*/'));
break;
case NullabilityFixKind.addLate:
case NullabilityFixKind.addLateDueToTestSetup:
@@ -363,7 +376,7 @@
}
var info = edit.info;
var edits = info != null
- ? _computeEdits(info, sourceOffset, result.content)
+ ? _computeEdits(info, sourceOffset, result)
: <EditDetail>[];
var lineNumber = lineInfo.getLocation(sourceOffset).lineNumber;
var traces = info == null
@@ -426,6 +439,17 @@
return unitInfo;
}
+ /// Searches [unit] for an import directive whose URI matches [uri], returning
+ /// it if found, or `null` if not found.
+ ImportDirective _findImportDirective(CompilationUnit unit, String uri) {
+ for (var directive in unit.directives) {
+ if (directive is ImportDirective && directive.uriContent == uri) {
+ return directive;
+ }
+ }
+ return null;
+ }
+
TraceEntryInfo _makeTraceEntry(
String description, CodeReference codeReference,
{List<HintAction> hintActions = const []}) {
diff --git a/pkg/nnbd_migration/lib/src/node_builder.dart b/pkg/nnbd_migration/lib/src/node_builder.dart
index 7baf8e8..ad52876 100644
--- a/pkg/nnbd_migration/lib/src/node_builder.dart
+++ b/pkg/nnbd_migration/lib/src/node_builder.dart
@@ -2,6 +2,7 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
+import 'package:_fe_analyzer_shared/src/scanner/token.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
@@ -228,11 +229,15 @@
@override
DecoratedType visitDefaultFormalParameter(DefaultFormalParameter node) {
var decoratedType = node.parameter.accept(this);
+ var hint = getPrefixHint(node.firstTokenAfterCommentAndMetadata);
if (node.defaultValue != null) {
node.defaultValue.accept(this);
return null;
} else if (node.declaredElement.hasRequired) {
return null;
+ } else if (hint != null && hint.kind == HintCommentKind.required) {
+ _variables.recordRequiredHint(source, node, hint);
+ return null;
}
if (decoratedType == null) {
throw StateError('No type computed for ${node.parameter.runtimeType} '
@@ -889,3 +894,40 @@
throw UnimplementedError(buffer.toString());
}
}
+
+extension on FormalParameter {
+ // TODO(srawlins): Add this to FormalParameter interface.
+ Token get firstTokenAfterCommentAndMetadata {
+ var parameter = this is DefaultFormalParameter
+ ? (this as DefaultFormalParameter).parameter
+ : this as NormalFormalParameter;
+ if (parameter is FieldFormalParameter) {
+ if (parameter.keyword != null) {
+ return parameter.keyword;
+ } else if (parameter.type != null) {
+ return parameter.type.beginToken;
+ } else {
+ return parameter.thisKeyword;
+ }
+ } else if (parameter is FunctionTypedFormalParameter) {
+ if (parameter.covariantKeyword != null) {
+ return parameter.covariantKeyword;
+ } else if (parameter.returnType != null) {
+ return parameter.returnType.beginToken;
+ } else {
+ return parameter.identifier.token;
+ }
+ } else if (parameter is SimpleFormalParameter) {
+ if (parameter.covariantKeyword != null) {
+ return parameter.covariantKeyword;
+ } else if (parameter.keyword != null) {
+ return parameter.keyword;
+ } else if (parameter.type != null) {
+ return parameter.type.beginToken;
+ } else {
+ return parameter.identifier.token;
+ }
+ }
+ return null;
+ }
+}
diff --git a/pkg/nnbd_migration/lib/src/utilities/hint_utils.dart b/pkg/nnbd_migration/lib/src/utilities/hint_utils.dart
index 471149c..28177af 100644
--- a/pkg/nnbd_migration/lib/src/utilities/hint_utils.dart
+++ b/pkg/nnbd_migration/lib/src/utilities/hint_utils.dart
@@ -67,6 +67,17 @@
lateOffset + 'late final'.length,
commentToken.end,
token.offset);
+ } else if (commentText == 'required') {
+ var requiredOffset =
+ commentOffset + commentToken.lexeme.indexOf('required');
+ return HintComment(
+ HintCommentKind.required,
+ commentOffset,
+ commentOffset,
+ requiredOffset,
+ requiredOffset + 'required'.length,
+ commentToken.end,
+ token.offset);
}
}
}
@@ -210,4 +221,8 @@
/// The comment `/*late final*/`, which indicates that the variable
/// declaration should be late and final.
lateFinal,
+
+ /// The comment `/*required*/`, which indicates that the parameter should be
+ /// required.
+ required,
}
diff --git a/pkg/nnbd_migration/lib/src/variables.dart b/pkg/nnbd_migration/lib/src/variables.dart
index c054c01..a3ddc70 100644
--- a/pkg/nnbd_migration/lib/src/variables.dart
+++ b/pkg/nnbd_migration/lib/src/variables.dart
@@ -62,6 +62,8 @@
final _nullabilityHints = <Source, Map<int, HintComment>>{};
+ final _requiredHints = <Source, Map<int, HintComment>>{};
+
final _unnecessaryCasts = <Source, Set<int>>{};
final AlreadyMigratedCodeDecorator _alreadyMigratedCodeDecorator;
@@ -185,6 +187,12 @@
{})[uniqueIdentifierForSpan(node.offset, node.end)];
}
+ /// If the given [node] is preceded by a `/*required*/` hint, returns the
+ /// HintComment for it; otherwise returns `null`. See [recordRequiredHint].
+ HintComment getRequiredHint(Source source, FormalParameter node) {
+ return (_requiredHints[source] ?? {})[node.offset];
+ }
+
/// If the given [expression] is followed by a null check hint (`/*!*/`),
/// returns the HintComment for it; otherwise returns `null`. See
/// [recordNullCheckHint].
@@ -260,6 +268,12 @@
{})[uniqueIdentifierForSpan(node.offset, node.end)] = hintComment;
}
+ /// Records that the given [node] was preceded by a `/*required*/` hint.
+ void recordRequiredHint(
+ Source source, FormalParameter node, HintComment hint) {
+ (_requiredHints[source] ??= {})[node.offset] = hint;
+ }
+
/// Records that the given [expression] is followed by a null check hint
/// (`/*!*/`), for later recall by [hasNullCheckHint].
void recordNullCheckHint(
diff --git a/pkg/nnbd_migration/test/edge_builder_test.dart b/pkg/nnbd_migration/test/edge_builder_test.dart
index fc33d2e..0497d63 100644
--- a/pkg/nnbd_migration/test/edge_builder_test.dart
+++ b/pkg/nnbd_migration/test/edge_builder_test.dart
@@ -3469,6 +3469,15 @@
}
Future<void>
+ test_functionDeclaration_parameter_named_no_default_required_hint() async {
+ await analyze('''
+void f({/*required*/ int i}) {}
+''');
+
+ assertNoUpstreamNullability(decoratedTypeAnnotation('int').node);
+ }
+
+ Future<void>
test_functionDeclaration_parameter_positionalOptional_default_notNull() async {
await analyze('''
void f([int i = 1]) {}
@@ -3618,6 +3627,20 @@
assertNoUpstreamNullability(nullable_i);
}
+ Future<void>
+ test_functionInvocation_parameter_named_missing_required_hint() async {
+ verifyNoTestUnitErrors = false;
+ await analyze('''
+void f({/*required*/ int i}) {}
+void g() {
+ f();
+}
+''');
+ // The call at `f()` is presumed to be in error; no constraint is recorded.
+ var nullable_i = decoratedTypeAnnotation('int i').node;
+ assertNoUpstreamNullability(nullable_i);
+ }
+
Future<void> test_functionInvocation_parameter_null() async {
await analyze('''
void f(int i) {}
diff --git a/pkg/nnbd_migration/test/front_end/info_builder_test.dart b/pkg/nnbd_migration/test/front_end/info_builder_test.dart
index cee694a..b4f76ed 100644
--- a/pkg/nnbd_migration/test/front_end/info_builder_test.dart
+++ b/pkg/nnbd_migration/test/front_end/info_builder_test.dart
@@ -796,7 +796,7 @@
edits: isEmpty);
}
- Future<void> test_insertedRequired_fieldFormal() async {
+ Future<void> test_insertedRequired_fieldFormal_hint() async {
var unit = await buildInfoForSingleTestFile('''
class C {
int level;
@@ -821,10 +821,41 @@
explanation: "Add 'required' keyword to parameter 'level' in 'C.'",
kind: NullabilityFixKind.addRequired);
assertEdit(
- edit: edits[0], offset: 42, length: 0, replacement: '@required ');
+ edit: edits[0], offset: 42, length: 0, replacement: '/*required*/ ');
}
- Future<void> test_insertedRequired_parameter() async {
+ Future<void> test_insertedRequired_fieldFormal() async {
+ addMetaPackage();
+ var unit = await buildInfoForSingleTestFile('''
+import 'package:meta/meta.dart';
+class C {
+ int level;
+ int level2;
+ C({this.level}) : this.level2 = level + 1;
+}
+''', migratedContent: '''
+import 'package:meta/meta.dart';
+class C {
+ int level;
+ int level2;
+ C({required this.level}) : this.level2 = level + 1;
+}
+''');
+ var regions = unit.fixRegions;
+ expect(regions, hasLength(1));
+ var region = regions[0];
+ var edits = region.edits;
+ assertRegion(
+ region: region,
+ offset: 77,
+ length: 9,
+ explanation: "Add 'required' keyword to parameter 'level' in 'C.'",
+ kind: NullabilityFixKind.addRequired);
+ assertEdit(
+ edit: edits[0], offset: 75, length: 0, replacement: '@required ');
+ }
+
+ Future<void> test_insertedRequired_parameter_hint() async {
var unit = await buildInfoForSingleTestFile('''
class C {
int level = 0;
@@ -847,7 +878,59 @@
explanation: "Add 'required' keyword to parameter 'lvl' in 'C.f'",
kind: NullabilityFixKind.addRequired);
assertEdit(
- edit: edits[0], offset: 37, length: 0, replacement: '@required ');
+ edit: edits[0], offset: 37, length: 0, replacement: '/*required*/ ');
+ }
+
+ Future<void> test_insertedRequired_parameter_metaPrefixed() async {
+ addMetaPackage();
+ var unit = await buildInfoForSingleTestFile('''
+import 'package:meta/meta.dart' as meta;
+class C {
+ int level = 0;
+ bool f({int lvl}) => lvl >= level;
+}
+''', migratedContent: '''
+import 'package:meta/meta.dart' as meta;
+class C {
+ int level = 0;
+ bool f({required int lvl}) => lvl >= level;
+}
+''');
+ var regions = unit.fixRegions;
+ expect(regions, hasLength(1));
+ var region = regions[0];
+ var edits = region.edits;
+ assertEdit(
+ edit: edits[0], offset: 78, length: 0, replacement: '@meta.required ');
+ }
+
+ Future<void> test_insertedRequired_parameter() async {
+ addMetaPackage();
+ var unit = await buildInfoForSingleTestFile('''
+import 'package:meta/meta.dart';
+class C {
+ int level = 0;
+ bool f({int lvl}) => lvl >= level;
+}
+''', migratedContent: '''
+import 'package:meta/meta.dart';
+class C {
+ int level = 0;
+ bool f({required int lvl}) => lvl >= level;
+}
+''');
+ var regions = unit.fixRegions;
+ expect(regions, hasLength(1));
+ var region = regions[0];
+ var edits = region.edits;
+ assertRegion(
+ region: region,
+ offset: 72,
+ length: 9,
+ explanation: "Add 'required' keyword to parameter 'lvl' in 'C.f'",
+ kind: NullabilityFixKind.addRequired);
+ assertEdit(
+ edit: edits[0], offset: 70, length: 0, replacement: '@required ');
}
Future<void> test_insertParens() async {
diff --git a/pkg/nnbd_migration/test/node_builder_test.dart b/pkg/nnbd_migration/test/node_builder_test.dart
index b85ac69..f729846 100644
--- a/pkg/nnbd_migration/test/node_builder_test.dart
+++ b/pkg/nnbd_migration/test/node_builder_test.dart
@@ -829,6 +829,62 @@
expect(ctorParamType.returnType.node.isImmutable, false);
}
+ Future<void>
+ test_fieldFormalParameter_named_no_default_required_hint() async {
+ await analyze('''
+class C {
+ String s;
+ C({/*required*/ this.s});
+}
+''');
+ expect(
+ variables.getRequiredHint(
+ testSource, findNode.fieldFormalParameter('this.s')),
+ isNotNull);
+ }
+
+ Future<void>
+ test_fieldFormalParameter_named_no_default_required_hint_annotation() async {
+ await analyze('''
+class C {
+ String s;
+ C({@deprecated /*required*/ this.s});
+}
+''');
+ expect(
+ variables.getRequiredHint(
+ testSource, findNode.fieldFormalParameter('this.s')),
+ isNotNull);
+ }
+
+ Future<void>
+ test_fieldFormalParameter_named_no_default_required_hint_function_typed() async {
+ await analyze('''
+class C {
+ String Function() s;
+ C({/*required*/ String this.s()});
+}
+''');
+ expect(
+ variables.getRequiredHint(
+ testSource, findNode.fieldFormalParameter('this.s')),
+ isNotNull);
+ }
+
+ Future<void>
+ test_fieldFormalParameter_named_no_default_required_hint_type() async {
+ await analyze('''
+class C {
+ String s;
+ C({/*required*/ String this.s});
+}
+''');
+ expect(
+ variables.getRequiredHint(
+ testSource, findNode.fieldFormalParameter('this.s')),
+ isNotNull);
+ }
+
Future<void> test_fieldFormalParameter_typed() async {
await analyze('''
class C {
@@ -863,6 +919,62 @@
// field.
}
+ Future<void> test_formalParameter_named_no_default_required_hint() async {
+ await analyze('''
+void f({/*required*/ String s}) {}
+''');
+ expect(variables.getRequiredHint(testSource, findNode.simpleParameter('s')),
+ isNotNull);
+ }
+
+ Future<void>
+ test_formalParameter_named_no_default_required_hint_annotation() async {
+ await analyze('''
+void f({@deprecated /*required*/ String s}) {}
+''');
+ expect(variables.getRequiredHint(testSource, findNode.simpleParameter('s')),
+ isNotNull);
+ }
+
+ Future<void>
+ test_formalParameter_named_no_default_required_hint_covariant() async {
+ await analyze('''
+class C {
+ void f({/*required*/ covariant String s}) {}
+}
+''');
+ expect(
+ variables.getRequiredHint(
+ testSource, findNode.simpleParameter('String s')),
+ isNotNull);
+ }
+
+ Future<void>
+ test_formalParameter_named_no_default_required_hint_final_type() async {
+ await analyze('''
+void f({/*required*/ final String s}) {}
+''');
+ expect(variables.getRequiredHint(testSource, findNode.simpleParameter('s')),
+ isNotNull);
+ }
+
+ Future<void>
+ test_formalParameter_named_no_default_required_hint_no_type() async {
+ await analyze('''
+void f({/*required*/ s}) {}
+''');
+ expect(variables.getRequiredHint(testSource, findNode.simpleParameter('s')),
+ isNotNull);
+ }
+
+ Future<void> test_formalParameter_named_no_default_required_hint_var() async {
+ await analyze('''
+void f({/*required*/ var s}) {}
+''');
+ expect(variables.getRequiredHint(testSource, findNode.simpleParameter('s')),
+ isNotNull);
+ }
+
Future<void> test_function_explicit_returnType() async {
await analyze('''
class C {
@@ -985,6 +1097,28 @@
same(decoratedTypeAnnotation('String')));
}
+ Future<void>
+ test_functionTypedFormalParameter_named_no_default_required_hint() async {
+ await analyze('''
+void f({/*required*/ void s()}) {}
+''');
+ expect(
+ variables.getRequiredHint(
+ testSource, findNode.functionTypedFormalParameter('s')),
+ isNotNull);
+ }
+
+ Future<void>
+ test_functionTypedFormalParameter_named_no_default_required_hint_no_return() async {
+ await analyze('''
+void f({/*required*/ s()}) {}
+''');
+ expect(
+ variables.getRequiredHint(
+ testSource, findNode.functionTypedFormalParameter('s')),
+ isNotNull);
+ }
+
Future<void> test_functionTypedFormalParameter_namedParameter_typed() async {
await analyze('''
void f(void g({int i})) {}
@@ -1701,6 +1835,24 @@
expect(functionType.namedParameters['s'].node.isPossiblyOptional, false);
}
+ Future<void>
+ test_topLevelFunction_parameterType_named_no_default_required_hint() 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.node, isNotNull);
+ expect(decoratedType.node, isNot(never));
+ expect(decoratedType.node, isNot(always));
+ expect(functionType.namedParameters['s'].node.isPossiblyOptional, false);
+ expect(variables.getRequiredHint(testSource, findNode.simpleParameter('s')),
+ isNotNull);
+ }
+
Future<void> test_topLevelFunction_parameterType_named_with_default() async {
await analyze('''
void f({String s: 'x'}) {}
@@ -2030,6 +2182,14 @@
expect(decorated.node, same(never));
}
+ Future<void> test_variableDeclaration_late_final_hint_simple() async {
+ await analyze('/*late final*/ int i;');
+ expect(
+ variables.getLateHint(
+ testSource, findNode.variableDeclarationList('int i')),
+ isNotNull);
+ }
+
Future<void> test_variableDeclaration_late_hint_after_metadata() async {
await analyze('@deprecated /*late*/ int i;');
expect(
@@ -2054,14 +2214,6 @@
isNotNull);
}
- Future<void> test_variableDeclaration_late_final_hint_simple() async {
- await analyze('/*late final*/ int i;');
- expect(
- variables.getLateHint(
- testSource, findNode.variableDeclarationList('int i')),
- isNotNull);
- }
-
Future<void> test_variableDeclaration_late_hint_with_spaces() async {
await analyze('/* late */ int i;');
expect(