Migration: Split up migration_visitor_test.dart.

This will make it easier to re-use the test infrastructure for some
other tests I plan to introduce in a follow-up CL.

Change-Id: I0a243f00f342674fe38274eb46e7c1e943817785
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/106442
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/nnbd_migration/test/migration_visitor_test.dart b/pkg/nnbd_migration/test/graph_builder_test.dart
similarity index 68%
rename from pkg/nnbd_migration/test/migration_visitor_test.dart
rename to pkg/nnbd_migration/test/graph_builder_test.dart
index cb2e1e0..e5305ea 100644
--- a/pkg/nnbd_migration/test/migration_visitor_test.dart
+++ b/pkg/nnbd_migration/test/graph_builder_test.dart
@@ -3,25 +3,18 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:analyzer/dart/ast/ast.dart';
-import 'package:analyzer/src/generated/resolver.dart';
-import 'package:analyzer/src/generated/source.dart';
-import 'package:meta/meta.dart';
-import 'package:nnbd_migration/src/conditional_discard.dart';
 import 'package:nnbd_migration/src/decorated_type.dart';
 import 'package:nnbd_migration/src/expression_checks.dart';
 import 'package:nnbd_migration/src/graph_builder.dart';
-import 'package:nnbd_migration/src/node_builder.dart';
 import 'package:nnbd_migration/src/nullability_node.dart';
-import 'package:nnbd_migration/src/variables.dart';
 import 'package:test/test.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
-import 'abstract_single_unit.dart';
+import 'migration_visitor_test_base.dart';
 
 main() {
   defineReflectiveSuite(() {
     defineReflectiveTests(GraphBuilderTest);
-    defineReflectiveTests(NodeBuilderTest);
   });
 }
 
@@ -32,8 +25,7 @@
   @override
   Future<CompilationUnit> analyze(String code) async {
     var unit = await super.analyze(code);
-    unit.accept(
-        GraphBuilder(typeProvider, _variables, graph, testSource, null));
+    unit.accept(GraphBuilder(typeProvider, variables, graph, testSource, null));
     return unit;
   }
 
@@ -69,14 +61,14 @@
   /// representation is [text], or `null` if the expression has no
   /// [ExpressionChecks] associated with it.
   ExpressionChecks checkExpression(String text) {
-    return _variables.checkExpression(findNode.expression(text));
+    return variables.checkExpression(findNode.expression(text));
   }
 
   /// Gets the [DecoratedType] associated with the expression whose text
   /// representation is [text], or `null` if the expression has no
   /// [DecoratedType] associated with it.
   DecoratedType decoratedExpressionType(String text) {
-    return _variables.decoratedExpressionType(findNode.expression(text));
+    return variables.decoratedExpressionType(findNode.expression(text));
   }
 
   test_assert_demonstrates_non_null_intent() async {
@@ -1552,492 +1544,3 @@
         hard: true);
   }
 }
-
-class MigrationVisitorTestBase extends AbstractSingleUnitTest {
-  final _Variables _variables;
-
-  final NullabilityGraphForTesting graph;
-
-  MigrationVisitorTestBase() : this._(NullabilityGraphForTesting());
-
-  MigrationVisitorTestBase._(this.graph) : _variables = _Variables(graph);
-
-  NullabilityNode get always => graph.always;
-
-  NullabilityNode get never => graph.never;
-
-  TypeProvider get typeProvider => testAnalysisResult.typeProvider;
-
-  Future<CompilationUnit> analyze(String code) async {
-    await resolveTestUnit(code);
-    testUnit
-        .accept(NodeBuilder(_variables, testSource, null, graph, typeProvider));
-    return testUnit;
-  }
-
-  NullabilityEdge assertEdge(
-      NullabilityNode source, NullabilityNode destination,
-      {@required bool hard, List<NullabilityNode> guards = const []}) {
-    var edges = getEdges(source, destination);
-    if (edges.length == 0) {
-      fail('Expected edge $source -> $destination, found none');
-    } else if (edges.length != 1) {
-      fail('Found multiple edges $source -> $destination');
-    } else {
-      var edge = edges[0];
-      expect(edge.hard, hard);
-      expect(edge.guards, unorderedEquals(guards));
-      return edge;
-    }
-  }
-
-  void assertNoEdge(NullabilityNode source, NullabilityNode destination) {
-    var edges = getEdges(source, destination);
-    if (edges.isNotEmpty) {
-      fail('Expected no edge $source -> $destination, found ${edges.length}');
-    }
-  }
-
-  void assertUnion(NullabilityNode x, NullabilityNode y) {
-    var edges = getEdges(x, y);
-    for (var edge in edges) {
-      if (edge.isUnion) {
-        expect(edge.sources, hasLength(1));
-        return;
-      }
-    }
-    fail('Expected union between $x and $y, not found');
-  }
-
-  /// Gets the [DecoratedType] associated with the generic function type
-  /// annotation whose text is [text].
-  DecoratedType decoratedGenericFunctionTypeAnnotation(String text) {
-    return _variables.decoratedTypeAnnotation(
-        testSource, findNode.genericFunctionType(text));
-  }
-
-  /// Gets the [DecoratedType] associated with the type annotation whose text
-  /// is [text].
-  DecoratedType decoratedTypeAnnotation(String text) {
-    return _variables.decoratedTypeAnnotation(
-        testSource, findNode.typeAnnotation(text));
-  }
-
-  List<NullabilityEdge> getEdges(
-          NullabilityNode source, NullabilityNode destination) =>
-      graph
-          .getUpstreamEdges(destination)
-          .where((e) => e.primarySource == source)
-          .toList();
-
-  NullabilityNode possiblyOptionalParameter(String text) {
-    return _variables
-        .possiblyOptionalParameter(findNode.defaultParameter(text));
-  }
-
-  /// Gets the [ConditionalDiscard] information associated with the statement
-  /// whose text is [text].
-  ConditionalDiscard statementDiscard(String text) {
-    return _variables.conditionalDiscard(findNode.statement(text));
-  }
-}
-
-@reflectiveTest
-class NodeBuilderTest extends MigrationVisitorTestBase {
-  /// Gets the [DecoratedType] associated with the constructor declaration whose
-  /// name matches [search].
-  DecoratedType decoratedConstructorDeclaration(String search) => _variables
-      .decoratedElementType(findNode.constructor(search).declaredElement);
-
-  /// Gets the [DecoratedType] associated with the function declaration whose
-  /// name matches [search].
-  DecoratedType decoratedFunctionType(String search) =>
-      _variables.decoratedElementType(
-          findNode.functionDeclaration(search).declaredElement);
-
-  DecoratedType decoratedTypeParameterBound(String search) => _variables
-      .decoratedElementType(findNode.typeParameter(search).declaredElement);
-
-  test_constructor_returnType_implicit_dynamic() async {
-    await analyze('''
-class C {
-  C();
-}
-''');
-    var decoratedType = decoratedConstructorDeclaration('C(').returnType;
-    expect(decoratedType.node, same(never));
-  }
-
-  test_dynamic_type() async {
-    await analyze('''
-dynamic f() {}
-''');
-    var decoratedType = decoratedTypeAnnotation('dynamic');
-    expect(decoratedFunctionType('f').returnType, same(decoratedType));
-    assertEdge(always, decoratedType.node, hard: false);
-  }
-
-  test_field_type_simple() async {
-    await analyze('''
-class C {
-  int f = 0;
-}
-''');
-    var decoratedType = decoratedTypeAnnotation('int');
-    expect(decoratedType.node, TypeMatcher<NullabilityNodeMutable>());
-    expect(
-        _variables.decoratedElementType(
-            findNode.fieldDeclaration('f').fields.variables[0].declaredElement),
-        same(decoratedType));
-  }
-
-  test_genericFunctionType_namedParameterType() async {
-    await analyze('''
-void f(void Function({int y}) x) {}
-''');
-    var decoratedType =
-        decoratedGenericFunctionTypeAnnotation('void Function({int y})');
-    expect(decoratedFunctionType('f').positionalParameters[0],
-        same(decoratedType));
-    expect(decoratedType.node, TypeMatcher<NullabilityNodeMutable>());
-    var decoratedIntType = decoratedTypeAnnotation('int');
-    expect(decoratedType.namedParameters['y'], same(decoratedIntType));
-    expect(decoratedIntType.node, isNotNull);
-    expect(decoratedIntType.node, isNot(never));
-  }
-
-  test_genericFunctionType_returnType() async {
-    await analyze('''
-void f(int Function() x) {}
-''');
-    var decoratedType =
-        decoratedGenericFunctionTypeAnnotation('int Function()');
-    expect(decoratedFunctionType('f').positionalParameters[0],
-        same(decoratedType));
-    expect(decoratedType.node, TypeMatcher<NullabilityNodeMutable>());
-    var decoratedIntType = decoratedTypeAnnotation('int');
-    expect(decoratedType.returnType, same(decoratedIntType));
-    expect(decoratedIntType.node, isNotNull);
-    expect(decoratedIntType.node, isNot(never));
-  }
-
-  test_genericFunctionType_unnamedParameterType() async {
-    await analyze('''
-void f(void Function(int) x) {}
-''');
-    var decoratedType =
-        decoratedGenericFunctionTypeAnnotation('void Function(int)');
-    expect(decoratedFunctionType('f').positionalParameters[0],
-        same(decoratedType));
-    expect(decoratedType.node, TypeMatcher<NullabilityNodeMutable>());
-    var decoratedIntType = decoratedTypeAnnotation('int');
-    expect(decoratedType.positionalParameters[0], same(decoratedIntType));
-    expect(decoratedIntType.node, isNotNull);
-    expect(decoratedIntType.node, isNot(never));
-  }
-
-  test_interfaceType_generic_instantiate_to_dynamic() async {
-    await analyze('''
-void f(List x) {}
-''');
-    var decoratedListType = decoratedTypeAnnotation('List');
-    expect(decoratedFunctionType('f').positionalParameters[0],
-        same(decoratedListType));
-    expect(decoratedListType.node, isNotNull);
-    expect(decoratedListType.node, isNot(never));
-    var decoratedArgType = decoratedListType.typeArguments[0];
-    expect(decoratedArgType.node, same(always));
-  }
-
-  test_interfaceType_generic_instantiate_to_generic_type() async {
-    await analyze('''
-class C<T> {}
-class D<T extends C<int>> {}
-void f(D x) {}
-''');
-    var decoratedListType = decoratedTypeAnnotation('D x');
-    expect(decoratedFunctionType('f').positionalParameters[0],
-        same(decoratedListType));
-    expect(decoratedListType.node, TypeMatcher<NullabilityNodeMutable>());
-    expect(decoratedListType.typeArguments, hasLength(1));
-    var decoratedArgType = decoratedListType.typeArguments[0];
-    expect(decoratedArgType.node, TypeMatcher<NullabilityNodeMutable>());
-    expect(decoratedArgType.typeArguments, hasLength(1));
-    var decoratedArgArgType = decoratedArgType.typeArguments[0];
-    expect(decoratedArgArgType.node, TypeMatcher<NullabilityNodeMutable>());
-    expect(decoratedArgArgType.typeArguments, isEmpty);
-  }
-
-  test_interfaceType_generic_instantiate_to_generic_type_2() async {
-    await analyze('''
-class C<T, U> {}
-class D<T extends C<int, String>, U extends C<num, double>> {}
-void f(D x) {}
-''');
-    var decoratedDType = decoratedTypeAnnotation('D x');
-    expect(decoratedFunctionType('f').positionalParameters[0],
-        same(decoratedDType));
-    expect(decoratedDType.node, TypeMatcher<NullabilityNodeMutable>());
-    expect(decoratedDType.typeArguments, hasLength(2));
-    var decoratedArg0Type = decoratedDType.typeArguments[0];
-    expect(decoratedArg0Type.node, TypeMatcher<NullabilityNodeMutable>());
-    expect(decoratedArg0Type.typeArguments, hasLength(2));
-    var decoratedArg0Arg0Type = decoratedArg0Type.typeArguments[0];
-    expect(decoratedArg0Arg0Type.node, TypeMatcher<NullabilityNodeMutable>());
-    expect(decoratedArg0Arg0Type.typeArguments, isEmpty);
-    var decoratedArg0Arg1Type = decoratedArg0Type.typeArguments[1];
-    expect(decoratedArg0Arg1Type.node, TypeMatcher<NullabilityNodeMutable>());
-    expect(decoratedArg0Arg1Type.typeArguments, isEmpty);
-    var decoratedArg1Type = decoratedDType.typeArguments[1];
-    expect(decoratedArg1Type.node, TypeMatcher<NullabilityNodeMutable>());
-    expect(decoratedArg1Type.typeArguments, hasLength(2));
-    var decoratedArg1Arg0Type = decoratedArg1Type.typeArguments[0];
-    expect(decoratedArg1Arg0Type.node, TypeMatcher<NullabilityNodeMutable>());
-    expect(decoratedArg1Arg0Type.typeArguments, isEmpty);
-    var decoratedArg1Arg1Type = decoratedArg1Type.typeArguments[1];
-    expect(decoratedArg1Arg1Type.node, TypeMatcher<NullabilityNodeMutable>());
-    expect(decoratedArg1Arg1Type.typeArguments, isEmpty);
-  }
-
-  test_interfaceType_generic_instantiate_to_object() async {
-    await analyze('''
-class C<T extends Object> {}
-void f(C x) {}
-''');
-    var decoratedListType = decoratedTypeAnnotation('C x');
-    expect(decoratedFunctionType('f').positionalParameters[0],
-        same(decoratedListType));
-    expect(decoratedListType.node, TypeMatcher<NullabilityNodeMutable>());
-    expect(decoratedListType.typeArguments, hasLength(1));
-    var decoratedArgType = decoratedListType.typeArguments[0];
-    expect(decoratedArgType.node, TypeMatcher<NullabilityNodeMutable>());
-    expect(decoratedArgType.typeArguments, isEmpty);
-  }
-
-  test_interfaceType_typeParameter() async {
-    await analyze('''
-void f(List<int> x) {}
-''');
-    var decoratedListType = decoratedTypeAnnotation('List<int>');
-    expect(decoratedFunctionType('f').positionalParameters[0],
-        same(decoratedListType));
-    expect(decoratedListType.node, isNotNull);
-    expect(decoratedListType.node, isNot(never));
-    var decoratedIntType = decoratedTypeAnnotation('int');
-    expect(decoratedListType.typeArguments[0], same(decoratedIntType));
-    expect(decoratedIntType.node, isNotNull);
-    expect(decoratedIntType.node, isNot(never));
-  }
-
-  test_topLevelFunction_parameterType_implicit_dynamic() async {
-    await analyze('''
-void f(x) {}
-''');
-    var decoratedType =
-        _variables.decoratedElementType(findNode.simple('x').staticElement);
-    expect(decoratedFunctionType('f').positionalParameters[0],
-        same(decoratedType));
-    expect(decoratedType.type.isDynamic, isTrue);
-    assertUnion(always, decoratedType.node);
-  }
-
-  test_topLevelFunction_parameterType_named_no_default() async {
-    await analyze('''
-void f({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, true);
-  }
-
-  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.node, isNotNull);
-    expect(decoratedType.node, isNot(never));
-    expect(decoratedType.node, isNot(always));
-    expect(functionType.namedParameters['s'].node.isPossiblyOptional, false);
-  }
-
-  test_topLevelFunction_parameterType_named_with_default() async {
-    await analyze('''
-void f({String s: 'x'}) {}
-''');
-    var decoratedType = decoratedTypeAnnotation('String');
-    var functionType = decoratedFunctionType('f');
-    expect(functionType.namedParameters['s'], same(decoratedType));
-    expect(decoratedType.node, isNotNull);
-    expect(decoratedType.node, isNot(never));
-    expect(functionType.namedParameters['s'].node.isPossiblyOptional, false);
-  }
-
-  test_topLevelFunction_parameterType_positionalOptional() async {
-    await analyze('''
-void f([int i]) {}
-''');
-    var decoratedType = decoratedTypeAnnotation('int');
-    expect(decoratedFunctionType('f').positionalParameters[0],
-        same(decoratedType));
-    expect(decoratedType.node, isNotNull);
-    expect(decoratedType.node, isNot(never));
-  }
-
-  test_topLevelFunction_parameterType_simple() async {
-    await analyze('''
-void f(int i) {}
-''');
-    var decoratedType = decoratedTypeAnnotation('int');
-    expect(decoratedFunctionType('f').positionalParameters[0],
-        same(decoratedType));
-    expect(decoratedType.node, isNotNull);
-    expect(decoratedType.node, isNot(never));
-  }
-
-  test_topLevelFunction_returnType_implicit_dynamic() async {
-    await analyze('''
-f() {}
-''');
-    var decoratedType = decoratedFunctionType('f').returnType;
-    expect(decoratedType.type.isDynamic, isTrue);
-    assertUnion(always, decoratedType.node);
-  }
-
-  test_topLevelFunction_returnType_simple() async {
-    await analyze('''
-int f() => 0;
-''');
-    var decoratedType = decoratedTypeAnnotation('int');
-    expect(decoratedFunctionType('f').returnType, same(decoratedType));
-    expect(decoratedType.node, isNotNull);
-    expect(decoratedType.node, isNot(never));
-  }
-
-  test_type_comment_bang() async {
-    await analyze('''
-void f(int/*!*/ i) {}
-''');
-    assertEdge(decoratedTypeAnnotation('int').node, never, hard: true);
-  }
-
-  test_type_comment_question() async {
-    await analyze('''
-void f(int/*?*/ i) {}
-''');
-    assertEdge(always, decoratedTypeAnnotation('int').node, hard: false);
-  }
-
-  test_type_parameter_explicit_bound() async {
-    await analyze('''
-class C<T extends Object> {}
-''');
-    var bound = decoratedTypeParameterBound('T');
-    expect(decoratedTypeAnnotation('Object'), same(bound));
-    expect(bound.node, isNot(always));
-    expect(bound.type, typeProvider.objectType);
-  }
-
-  test_type_parameter_implicit_bound() async {
-    // The implicit bound of `T` is automatically `Object?`.  TODO(paulberry):
-    // consider making it possible for type inference to infer an explicit bound
-    // of `Object`.
-    await analyze('''
-class C<T> {}
-''');
-    var bound = decoratedTypeParameterBound('T');
-    assertUnion(always, bound.node);
-    expect(bound.type, same(typeProvider.objectType));
-  }
-
-  test_variableDeclaration_type_simple() async {
-    await analyze('''
-main() {
-  int i;
-}
-''');
-    var decoratedType = decoratedTypeAnnotation('int');
-    expect(decoratedType.node, TypeMatcher<NullabilityNodeMutable>());
-  }
-
-  test_void_type() async {
-    await analyze('''
-void f() {}
-''');
-    var decoratedType = decoratedTypeAnnotation('void');
-    expect(decoratedFunctionType('f').returnType, same(decoratedType));
-    assertEdge(always, decoratedType.node, hard: false);
-  }
-}
-
-/// Mock representation of constraint variables.
-class _Variables extends Variables {
-  final _conditionalDiscard = <AstNode, ConditionalDiscard>{};
-
-  final _decoratedExpressionTypes = <Expression, DecoratedType>{};
-
-  final _expressionChecks = <Expression, ExpressionChecks>{};
-
-  final _possiblyOptional = <DefaultFormalParameter, NullabilityNode>{};
-
-  _Variables(NullabilityGraph graph) : super(graph);
-
-  /// Gets the [ExpressionChecks] associated with the given [expression].
-  ExpressionChecks checkExpression(Expression expression) =>
-      _expressionChecks[_normalizeExpression(expression)];
-
-  /// Gets the [conditionalDiscard] associated with the given [expression].
-  ConditionalDiscard conditionalDiscard(AstNode node) =>
-      _conditionalDiscard[node];
-
-  /// Gets the [DecoratedType] associated with the given [expression].
-  DecoratedType decoratedExpressionType(Expression expression) =>
-      _decoratedExpressionTypes[_normalizeExpression(expression)];
-
-  /// Gets the [NullabilityNode] associated with the possibility that
-  /// [parameter] may be optional.
-  NullabilityNode possiblyOptionalParameter(DefaultFormalParameter parameter) =>
-      _possiblyOptional[parameter];
-
-  @override
-  void recordConditionalDiscard(
-      Source source, AstNode node, ConditionalDiscard conditionalDiscard) {
-    _conditionalDiscard[node] = conditionalDiscard;
-    super.recordConditionalDiscard(source, node, conditionalDiscard);
-  }
-
-  void recordDecoratedExpressionType(Expression node, DecoratedType type) {
-    super.recordDecoratedExpressionType(node, type);
-    _decoratedExpressionTypes[_normalizeExpression(node)] = type;
-  }
-
-  @override
-  void recordExpressionChecks(
-      Source source, Expression expression, ExpressionChecks checks) {
-    super.recordExpressionChecks(source, expression, checks);
-    _expressionChecks[_normalizeExpression(expression)] = checks;
-  }
-
-  @override
-  void recordPossiblyOptional(
-      Source source, DefaultFormalParameter parameter, NullabilityNode node) {
-    _possiblyOptional[parameter] = node;
-    super.recordPossiblyOptional(source, parameter, node);
-  }
-
-  /// Unwraps any parentheses surrounding [expression].
-  Expression _normalizeExpression(Expression expression) {
-    while (expression is ParenthesizedExpression) {
-      expression = (expression as ParenthesizedExpression).expression;
-    }
-    return expression;
-  }
-}
diff --git a/pkg/nnbd_migration/test/migration_visitor_test_base.dart b/pkg/nnbd_migration/test/migration_visitor_test_base.dart
new file mode 100644
index 0000000..2e2e9d1
--- /dev/null
+++ b/pkg/nnbd_migration/test/migration_visitor_test_base.dart
@@ -0,0 +1,170 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// 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:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/src/generated/resolver.dart';
+import 'package:analyzer/src/generated/source.dart';
+import 'package:meta/meta.dart';
+import 'package:nnbd_migration/src/conditional_discard.dart';
+import 'package:nnbd_migration/src/decorated_type.dart';
+import 'package:nnbd_migration/src/expression_checks.dart';
+import 'package:nnbd_migration/src/node_builder.dart';
+import 'package:nnbd_migration/src/nullability_node.dart';
+import 'package:nnbd_migration/src/variables.dart';
+import 'package:test/test.dart';
+
+import 'abstract_single_unit.dart';
+
+/// Mock representation of constraint variables.
+class InstrumentedVariables extends Variables {
+  final _conditionalDiscard = <AstNode, ConditionalDiscard>{};
+
+  final _decoratedExpressionTypes = <Expression, DecoratedType>{};
+
+  final _expressionChecks = <Expression, ExpressionChecks>{};
+
+  final _possiblyOptional = <DefaultFormalParameter, NullabilityNode>{};
+
+  InstrumentedVariables(NullabilityGraph graph) : super(graph);
+
+  /// Gets the [ExpressionChecks] associated with the given [expression].
+  ExpressionChecks checkExpression(Expression expression) =>
+      _expressionChecks[_normalizeExpression(expression)];
+
+  /// Gets the [conditionalDiscard] associated with the given [expression].
+  ConditionalDiscard conditionalDiscard(AstNode node) =>
+      _conditionalDiscard[node];
+
+  /// Gets the [DecoratedType] associated with the given [expression].
+  DecoratedType decoratedExpressionType(Expression expression) =>
+      _decoratedExpressionTypes[_normalizeExpression(expression)];
+
+  /// Gets the [NullabilityNode] associated with the possibility that
+  /// [parameter] may be optional.
+  NullabilityNode possiblyOptionalParameter(DefaultFormalParameter parameter) =>
+      _possiblyOptional[parameter];
+
+  @override
+  void recordConditionalDiscard(
+      Source source, AstNode node, ConditionalDiscard conditionalDiscard) {
+    _conditionalDiscard[node] = conditionalDiscard;
+    super.recordConditionalDiscard(source, node, conditionalDiscard);
+  }
+
+  void recordDecoratedExpressionType(Expression node, DecoratedType type) {
+    super.recordDecoratedExpressionType(node, type);
+    _decoratedExpressionTypes[_normalizeExpression(node)] = type;
+  }
+
+  @override
+  void recordExpressionChecks(
+      Source source, Expression expression, ExpressionChecks checks) {
+    super.recordExpressionChecks(source, expression, checks);
+    _expressionChecks[_normalizeExpression(expression)] = checks;
+  }
+
+  @override
+  void recordPossiblyOptional(
+      Source source, DefaultFormalParameter parameter, NullabilityNode node) {
+    _possiblyOptional[parameter] = node;
+    super.recordPossiblyOptional(source, parameter, node);
+  }
+
+  /// Unwraps any parentheses surrounding [expression].
+  Expression _normalizeExpression(Expression expression) {
+    while (expression is ParenthesizedExpression) {
+      expression = (expression as ParenthesizedExpression).expression;
+    }
+    return expression;
+  }
+}
+
+class MigrationVisitorTestBase extends AbstractSingleUnitTest {
+  final InstrumentedVariables variables;
+
+  final NullabilityGraphForTesting graph;
+
+  MigrationVisitorTestBase() : this._(NullabilityGraphForTesting());
+
+  MigrationVisitorTestBase._(this.graph)
+      : variables = InstrumentedVariables(graph);
+
+  NullabilityNode get always => graph.always;
+
+  NullabilityNode get never => graph.never;
+
+  TypeProvider get typeProvider => testAnalysisResult.typeProvider;
+
+  Future<CompilationUnit> analyze(String code) async {
+    await resolveTestUnit(code);
+    testUnit
+        .accept(NodeBuilder(variables, testSource, null, graph, typeProvider));
+    return testUnit;
+  }
+
+  NullabilityEdge assertEdge(
+      NullabilityNode source, NullabilityNode destination,
+      {@required bool hard, List<NullabilityNode> guards = const []}) {
+    var edges = getEdges(source, destination);
+    if (edges.length == 0) {
+      fail('Expected edge $source -> $destination, found none');
+    } else if (edges.length != 1) {
+      fail('Found multiple edges $source -> $destination');
+    } else {
+      var edge = edges[0];
+      expect(edge.hard, hard);
+      expect(edge.guards, unorderedEquals(guards));
+      return edge;
+    }
+  }
+
+  void assertNoEdge(NullabilityNode source, NullabilityNode destination) {
+    var edges = getEdges(source, destination);
+    if (edges.isNotEmpty) {
+      fail('Expected no edge $source -> $destination, found ${edges.length}');
+    }
+  }
+
+  void assertUnion(NullabilityNode x, NullabilityNode y) {
+    var edges = getEdges(x, y);
+    for (var edge in edges) {
+      if (edge.isUnion) {
+        expect(edge.sources, hasLength(1));
+        return;
+      }
+    }
+    fail('Expected union between $x and $y, not found');
+  }
+
+  /// Gets the [DecoratedType] associated with the generic function type
+  /// annotation whose text is [text].
+  DecoratedType decoratedGenericFunctionTypeAnnotation(String text) {
+    return variables.decoratedTypeAnnotation(
+        testSource, findNode.genericFunctionType(text));
+  }
+
+  /// Gets the [DecoratedType] associated with the type annotation whose text
+  /// is [text].
+  DecoratedType decoratedTypeAnnotation(String text) {
+    return variables.decoratedTypeAnnotation(
+        testSource, findNode.typeAnnotation(text));
+  }
+
+  List<NullabilityEdge> getEdges(
+          NullabilityNode source, NullabilityNode destination) =>
+      graph
+          .getUpstreamEdges(destination)
+          .where((e) => e.primarySource == source)
+          .toList();
+
+  NullabilityNode possiblyOptionalParameter(String text) {
+    return variables.possiblyOptionalParameter(findNode.defaultParameter(text));
+  }
+
+  /// Gets the [ConditionalDiscard] information associated with the statement
+  /// whose text is [text].
+  ConditionalDiscard statementDiscard(String text) {
+    return variables.conditionalDiscard(findNode.statement(text));
+  }
+}
diff --git a/pkg/nnbd_migration/test/node_builder_test.dart b/pkg/nnbd_migration/test/node_builder_test.dart
new file mode 100644
index 0000000..ef4476f
--- /dev/null
+++ b/pkg/nnbd_migration/test/node_builder_test.dart
@@ -0,0 +1,352 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// 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:nnbd_migration/src/decorated_type.dart';
+import 'package:nnbd_migration/src/nullability_node.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'migration_visitor_test_base.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(NodeBuilderTest);
+  });
+}
+
+@reflectiveTest
+class NodeBuilderTest extends MigrationVisitorTestBase {
+  /// Gets the [DecoratedType] associated with the constructor declaration whose
+  /// name matches [search].
+  DecoratedType decoratedConstructorDeclaration(String search) => variables
+      .decoratedElementType(findNode.constructor(search).declaredElement);
+
+  /// Gets the [DecoratedType] associated with the function declaration whose
+  /// name matches [search].
+  DecoratedType decoratedFunctionType(String search) =>
+      variables.decoratedElementType(
+          findNode.functionDeclaration(search).declaredElement);
+
+  DecoratedType decoratedTypeParameterBound(String search) => variables
+      .decoratedElementType(findNode.typeParameter(search).declaredElement);
+
+  test_constructor_returnType_implicit_dynamic() async {
+    await analyze('''
+class C {
+  C();
+}
+''');
+    var decoratedType = decoratedConstructorDeclaration('C(').returnType;
+    expect(decoratedType.node, same(never));
+  }
+
+  test_dynamic_type() async {
+    await analyze('''
+dynamic f() {}
+''');
+    var decoratedType = decoratedTypeAnnotation('dynamic');
+    expect(decoratedFunctionType('f').returnType, same(decoratedType));
+    assertEdge(always, decoratedType.node, hard: false);
+  }
+
+  test_field_type_simple() async {
+    await analyze('''
+class C {
+  int f = 0;
+}
+''');
+    var decoratedType = decoratedTypeAnnotation('int');
+    expect(decoratedType.node, TypeMatcher<NullabilityNodeMutable>());
+    expect(
+        variables.decoratedElementType(
+            findNode.fieldDeclaration('f').fields.variables[0].declaredElement),
+        same(decoratedType));
+  }
+
+  test_genericFunctionType_namedParameterType() async {
+    await analyze('''
+void f(void Function({int y}) x) {}
+''');
+    var decoratedType =
+        decoratedGenericFunctionTypeAnnotation('void Function({int y})');
+    expect(decoratedFunctionType('f').positionalParameters[0],
+        same(decoratedType));
+    expect(decoratedType.node, TypeMatcher<NullabilityNodeMutable>());
+    var decoratedIntType = decoratedTypeAnnotation('int');
+    expect(decoratedType.namedParameters['y'], same(decoratedIntType));
+    expect(decoratedIntType.node, isNotNull);
+    expect(decoratedIntType.node, isNot(never));
+  }
+
+  test_genericFunctionType_returnType() async {
+    await analyze('''
+void f(int Function() x) {}
+''');
+    var decoratedType =
+        decoratedGenericFunctionTypeAnnotation('int Function()');
+    expect(decoratedFunctionType('f').positionalParameters[0],
+        same(decoratedType));
+    expect(decoratedType.node, TypeMatcher<NullabilityNodeMutable>());
+    var decoratedIntType = decoratedTypeAnnotation('int');
+    expect(decoratedType.returnType, same(decoratedIntType));
+    expect(decoratedIntType.node, isNotNull);
+    expect(decoratedIntType.node, isNot(never));
+  }
+
+  test_genericFunctionType_unnamedParameterType() async {
+    await analyze('''
+void f(void Function(int) x) {}
+''');
+    var decoratedType =
+        decoratedGenericFunctionTypeAnnotation('void Function(int)');
+    expect(decoratedFunctionType('f').positionalParameters[0],
+        same(decoratedType));
+    expect(decoratedType.node, TypeMatcher<NullabilityNodeMutable>());
+    var decoratedIntType = decoratedTypeAnnotation('int');
+    expect(decoratedType.positionalParameters[0], same(decoratedIntType));
+    expect(decoratedIntType.node, isNotNull);
+    expect(decoratedIntType.node, isNot(never));
+  }
+
+  test_interfaceType_generic_instantiate_to_dynamic() async {
+    await analyze('''
+void f(List x) {}
+''');
+    var decoratedListType = decoratedTypeAnnotation('List');
+    expect(decoratedFunctionType('f').positionalParameters[0],
+        same(decoratedListType));
+    expect(decoratedListType.node, isNotNull);
+    expect(decoratedListType.node, isNot(never));
+    var decoratedArgType = decoratedListType.typeArguments[0];
+    expect(decoratedArgType.node, same(always));
+  }
+
+  test_interfaceType_generic_instantiate_to_generic_type() async {
+    await analyze('''
+class C<T> {}
+class D<T extends C<int>> {}
+void f(D x) {}
+''');
+    var decoratedListType = decoratedTypeAnnotation('D x');
+    expect(decoratedFunctionType('f').positionalParameters[0],
+        same(decoratedListType));
+    expect(decoratedListType.node, TypeMatcher<NullabilityNodeMutable>());
+    expect(decoratedListType.typeArguments, hasLength(1));
+    var decoratedArgType = decoratedListType.typeArguments[0];
+    expect(decoratedArgType.node, TypeMatcher<NullabilityNodeMutable>());
+    expect(decoratedArgType.typeArguments, hasLength(1));
+    var decoratedArgArgType = decoratedArgType.typeArguments[0];
+    expect(decoratedArgArgType.node, TypeMatcher<NullabilityNodeMutable>());
+    expect(decoratedArgArgType.typeArguments, isEmpty);
+  }
+
+  test_interfaceType_generic_instantiate_to_generic_type_2() async {
+    await analyze('''
+class C<T, U> {}
+class D<T extends C<int, String>, U extends C<num, double>> {}
+void f(D x) {}
+''');
+    var decoratedDType = decoratedTypeAnnotation('D x');
+    expect(decoratedFunctionType('f').positionalParameters[0],
+        same(decoratedDType));
+    expect(decoratedDType.node, TypeMatcher<NullabilityNodeMutable>());
+    expect(decoratedDType.typeArguments, hasLength(2));
+    var decoratedArg0Type = decoratedDType.typeArguments[0];
+    expect(decoratedArg0Type.node, TypeMatcher<NullabilityNodeMutable>());
+    expect(decoratedArg0Type.typeArguments, hasLength(2));
+    var decoratedArg0Arg0Type = decoratedArg0Type.typeArguments[0];
+    expect(decoratedArg0Arg0Type.node, TypeMatcher<NullabilityNodeMutable>());
+    expect(decoratedArg0Arg0Type.typeArguments, isEmpty);
+    var decoratedArg0Arg1Type = decoratedArg0Type.typeArguments[1];
+    expect(decoratedArg0Arg1Type.node, TypeMatcher<NullabilityNodeMutable>());
+    expect(decoratedArg0Arg1Type.typeArguments, isEmpty);
+    var decoratedArg1Type = decoratedDType.typeArguments[1];
+    expect(decoratedArg1Type.node, TypeMatcher<NullabilityNodeMutable>());
+    expect(decoratedArg1Type.typeArguments, hasLength(2));
+    var decoratedArg1Arg0Type = decoratedArg1Type.typeArguments[0];
+    expect(decoratedArg1Arg0Type.node, TypeMatcher<NullabilityNodeMutable>());
+    expect(decoratedArg1Arg0Type.typeArguments, isEmpty);
+    var decoratedArg1Arg1Type = decoratedArg1Type.typeArguments[1];
+    expect(decoratedArg1Arg1Type.node, TypeMatcher<NullabilityNodeMutable>());
+    expect(decoratedArg1Arg1Type.typeArguments, isEmpty);
+  }
+
+  test_interfaceType_generic_instantiate_to_object() async {
+    await analyze('''
+class C<T extends Object> {}
+void f(C x) {}
+''');
+    var decoratedListType = decoratedTypeAnnotation('C x');
+    expect(decoratedFunctionType('f').positionalParameters[0],
+        same(decoratedListType));
+    expect(decoratedListType.node, TypeMatcher<NullabilityNodeMutable>());
+    expect(decoratedListType.typeArguments, hasLength(1));
+    var decoratedArgType = decoratedListType.typeArguments[0];
+    expect(decoratedArgType.node, TypeMatcher<NullabilityNodeMutable>());
+    expect(decoratedArgType.typeArguments, isEmpty);
+  }
+
+  test_interfaceType_typeParameter() async {
+    await analyze('''
+void f(List<int> x) {}
+''');
+    var decoratedListType = decoratedTypeAnnotation('List<int>');
+    expect(decoratedFunctionType('f').positionalParameters[0],
+        same(decoratedListType));
+    expect(decoratedListType.node, isNotNull);
+    expect(decoratedListType.node, isNot(never));
+    var decoratedIntType = decoratedTypeAnnotation('int');
+    expect(decoratedListType.typeArguments[0], same(decoratedIntType));
+    expect(decoratedIntType.node, isNotNull);
+    expect(decoratedIntType.node, isNot(never));
+  }
+
+  test_topLevelFunction_parameterType_implicit_dynamic() async {
+    await analyze('''
+void f(x) {}
+''');
+    var decoratedType =
+        variables.decoratedElementType(findNode.simple('x').staticElement);
+    expect(decoratedFunctionType('f').positionalParameters[0],
+        same(decoratedType));
+    expect(decoratedType.type.isDynamic, isTrue);
+    assertUnion(always, decoratedType.node);
+  }
+
+  test_topLevelFunction_parameterType_named_no_default() async {
+    await analyze('''
+void f({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, true);
+  }
+
+  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.node, isNotNull);
+    expect(decoratedType.node, isNot(never));
+    expect(decoratedType.node, isNot(always));
+    expect(functionType.namedParameters['s'].node.isPossiblyOptional, false);
+  }
+
+  test_topLevelFunction_parameterType_named_with_default() async {
+    await analyze('''
+void f({String s: 'x'}) {}
+''');
+    var decoratedType = decoratedTypeAnnotation('String');
+    var functionType = decoratedFunctionType('f');
+    expect(functionType.namedParameters['s'], same(decoratedType));
+    expect(decoratedType.node, isNotNull);
+    expect(decoratedType.node, isNot(never));
+    expect(functionType.namedParameters['s'].node.isPossiblyOptional, false);
+  }
+
+  test_topLevelFunction_parameterType_positionalOptional() async {
+    await analyze('''
+void f([int i]) {}
+''');
+    var decoratedType = decoratedTypeAnnotation('int');
+    expect(decoratedFunctionType('f').positionalParameters[0],
+        same(decoratedType));
+    expect(decoratedType.node, isNotNull);
+    expect(decoratedType.node, isNot(never));
+  }
+
+  test_topLevelFunction_parameterType_simple() async {
+    await analyze('''
+void f(int i) {}
+''');
+    var decoratedType = decoratedTypeAnnotation('int');
+    expect(decoratedFunctionType('f').positionalParameters[0],
+        same(decoratedType));
+    expect(decoratedType.node, isNotNull);
+    expect(decoratedType.node, isNot(never));
+  }
+
+  test_topLevelFunction_returnType_implicit_dynamic() async {
+    await analyze('''
+f() {}
+''');
+    var decoratedType = decoratedFunctionType('f').returnType;
+    expect(decoratedType.type.isDynamic, isTrue);
+    assertUnion(always, decoratedType.node);
+  }
+
+  test_topLevelFunction_returnType_simple() async {
+    await analyze('''
+int f() => 0;
+''');
+    var decoratedType = decoratedTypeAnnotation('int');
+    expect(decoratedFunctionType('f').returnType, same(decoratedType));
+    expect(decoratedType.node, isNotNull);
+    expect(decoratedType.node, isNot(never));
+  }
+
+  test_type_comment_bang() async {
+    await analyze('''
+void f(int/*!*/ i) {}
+''');
+    assertEdge(decoratedTypeAnnotation('int').node, never, hard: true);
+  }
+
+  test_type_comment_question() async {
+    await analyze('''
+void f(int/*?*/ i) {}
+''');
+    assertEdge(always, decoratedTypeAnnotation('int').node, hard: false);
+  }
+
+  test_type_parameter_explicit_bound() async {
+    await analyze('''
+class C<T extends Object> {}
+''');
+    var bound = decoratedTypeParameterBound('T');
+    expect(decoratedTypeAnnotation('Object'), same(bound));
+    expect(bound.node, isNot(always));
+    expect(bound.type, typeProvider.objectType);
+  }
+
+  test_type_parameter_implicit_bound() async {
+    // The implicit bound of `T` is automatically `Object?`.  TODO(paulberry):
+    // consider making it possible for type inference to infer an explicit bound
+    // of `Object`.
+    await analyze('''
+class C<T> {}
+''');
+    var bound = decoratedTypeParameterBound('T');
+    assertUnion(always, bound.node);
+    expect(bound.type, same(typeProvider.objectType));
+  }
+
+  test_variableDeclaration_type_simple() async {
+    await analyze('''
+main() {
+  int i;
+}
+''');
+    var decoratedType = decoratedTypeAnnotation('int');
+    expect(decoratedType.node, TypeMatcher<NullabilityNodeMutable>());
+  }
+
+  test_void_type() async {
+    await analyze('''
+void f() {}
+''');
+    var decoratedType = decoratedTypeAnnotation('void');
+    expect(decoratedFunctionType('f').returnType, same(decoratedType));
+    assertEdge(always, decoratedType.node, hard: false);
+  }
+}
diff --git a/pkg/nnbd_migration/test/test_all.dart b/pkg/nnbd_migration/test/test_all.dart
index af35c52..4241cf2 100644
--- a/pkg/nnbd_migration/test/test_all.dart
+++ b/pkg/nnbd_migration/test/test_all.dart
@@ -5,13 +5,15 @@
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
 import 'api_test.dart' as api_test;
-import 'migration_visitor_test.dart' as migration_visitor_test;
+import 'graph_builder_test.dart' as graph_builder_test;
+import 'node_builder_test.dart' as node_builder_test;
 import 'nullability_node_test.dart' as nullability_node_test;
 
 main() {
   defineReflectiveSuite(() {
     api_test.main();
-    migration_visitor_test.main();
+    graph_builder_test.main();
+    node_builder_test.main();
     nullability_node_test.main();
   });
 }