blob: 5ed5febadaf90c3f1fa926c443a48088dd9ab065 [file] [log] [blame]
// 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/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type_provider.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/utilities_dart.dart';
import 'package:meta/meta.dart';
import 'package:nnbd_migration/instrumentation.dart';
import 'package:nnbd_migration/src/conditional_discard.dart';
import 'package:nnbd_migration/src/decorated_class_hierarchy.dart';
import 'package:nnbd_migration/src/decorated_type.dart';
import 'package:nnbd_migration/src/edge_builder.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/nullability_node_target.dart';
import 'package:nnbd_migration/src/variables.dart';
import 'package:test/test.dart';
import 'abstract_single_unit.dart';
/// A [NodeMatcher] that matches any node, and records what node it matched to.
class AnyNodeMatcher extends _RecordingNodeMatcher {
bool matches(NullabilityNode node) {
return true;
/// Mixin allowing unit tests to create decorated types easily.
mixin DecoratedTypeTester implements DecoratedTypeTesterBase {
int nodeId = 0;
NullabilityNode get always => graph.always;
DecoratedType get bottom => DecoratedType(typeProvider.bottomType, never);
DecoratedType get dynamic_ => DecoratedType(typeProvider.dynamicType, always);
NullabilityNode get never => graph.never;
DecoratedType get null_ => DecoratedType(typeProvider.nullType, always);
DecoratedType get void_ => DecoratedType(typeProvider.voidType, always);
DecoratedType function(DecoratedType returnType,
{List<DecoratedType> required = const [],
List<DecoratedType> positional = const [],
Map<String, DecoratedType> named = const {},
List<TypeParameterElement> typeFormals = const [],
NullabilityNode node}) {
int i = 0;
var parameters = required
.map((t) => ParameterElementImpl.synthetic(
'p${i++}', t.type, ParameterKind.REQUIRED))
parameters.addAll( => ParameterElementImpl.synthetic(
'p${i++}', t.type, ParameterKind.POSITIONAL)));
parameters.addAll( => ParameterElementImpl.synthetic(
e.key, e.value.type, ParameterKind.NAMED)));
return DecoratedType(
typeFormals: typeFormals,
parameters: parameters,
returnType: returnType.type,
node ?? newNode(),
returnType: returnType,
positionalParameters: required.toList()..addAll(positional),
namedParameters: named);
DecoratedType future(DecoratedType parameter, {NullabilityNode node}) {
return DecoratedType(
typeProvider.futureType2(parameter.type), node ?? newNode(),
typeArguments: [parameter]);
DecoratedType futureOr(DecoratedType parameter, {NullabilityNode node}) {
return DecoratedType(
typeProvider.futureOrType2(parameter.type), node ?? newNode(),
typeArguments: [parameter]);
DecoratedType int_({NullabilityNode node}) =>
DecoratedType(typeProvider.intType, node ?? newNode());
DecoratedType iterable(DecoratedType elementType, {NullabilityNode node}) =>
typeProvider.iterableType2(elementType.type), node ?? newNode(),
typeArguments: [elementType]);
DecoratedType list(DecoratedType elementType, {NullabilityNode node}) =>
DecoratedType(typeProvider.listType2(elementType.type), node ?? newNode(),
typeArguments: [elementType]);
NullabilityNode newNode() => NullabilityNode.forTypeAnnotation(
NullabilityNodeTarget.text('node ${nodeId++}'));
DecoratedType num_({NullabilityNode node}) =>
DecoratedType(typeProvider.numType, node ?? newNode());
DecoratedType object({NullabilityNode node}) =>
DecoratedType(typeProvider.objectType, node ?? newNode());
TypeParameterElement typeParameter(String name, DecoratedType bound) {
var element = TypeParameterElementImpl.synthetic(name);
element.bound = bound.type;
decoratedTypeParameterBounds.put(element, bound);
return element;
DecoratedType typeParameterType(TypeParameterElement typeParameter,
{NullabilityNode node}) {
return DecoratedType(
node ?? newNode(),
/// Base functionality that must be implemented by classes mixing in
/// [DecoratedTypeTester].
abstract class DecoratedTypeTesterBase {
DecoratedTypeParameterBounds get decoratedTypeParameterBounds;
NullabilityGraph get graph;
TypeProvider get typeProvider;
class EdgeBuilderTestBase extends MigrationVisitorTestBase {
DecoratedClassHierarchy decoratedClassHierarchy;
/// Analyzes the given source code, producing constraint variables and
/// constraints for it.
Future<CompilationUnit> analyze(String code) async {
var unit = await super.analyze(code);
decoratedClassHierarchy = DecoratedClassHierarchy(variables, graph);
unit.accept(EdgeBuilder(typeProvider, typeSystem, variables, graph,
testSource, null, decoratedClassHierarchy));
return unit;
/// Mixin allowing unit tests to check for the presence of graph edges.
mixin EdgeTester {
/// Gets the set of all nodes pointed to by always, plus always itself.
Set<NullabilityNode> get alwaysPlus {
var result = <NullabilityNode>{graph.always};
for (var edge in getEdges(graph.always, anyNode)) {
if (edge.guards.isEmpty) {
return result;
/// Returns a [NodeMatcher] that matches any node whatsoever.
AnyNodeMatcher get anyNode => AnyNodeMatcher();
NullabilityGraphForTesting get graph;
/// Gets the transitive closure of all nodes with hard edges pointing to
/// never, plus never itself.
Set<NullabilityNode> get neverClosure {
var result = <NullabilityNode>{};
var pending = <NullabilityNode>[graph.never];
while (pending.isNotEmpty) {
var node = pending.removeLast();
if (result.add(node)) {
for (var edge in getEdges(anyNode, node)) {
return result;
/// Gets the set of nodes with hard edges pointing to never.
Set<NullabilityNode> get pointsToNever {
return {for (var edge in getEdges(anyNode, graph.never)) edge.sourceNode};
/// Asserts that a dummy edge exists from [source] to always.
NullabilityEdge assertDummyEdge(Object source) =>
assertEdge(source, graph.always, hard: false, checkable: false);
/// Asserts that an edge exists with a node matching [source] and a node
/// matching [destination], and with the given [hard]ness and [guards].
/// [source] and [destination] are converted to [NodeMatcher] objects if they
/// aren't already. In practice this means that the caller can pass in either
/// a [NodeMatcher] or a [NullabilityNode].
NullabilityEdge assertEdge(Object source, Object destination,
{@required bool hard,
bool checkable = true,
bool isSetupAssignment = false,
Object guards = isEmpty,
Object codeReference}) {
var edges = getEdges(source, destination);
if (edges.isEmpty) {
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.isHard, hard);
expect(edge.isCheckable, checkable);
expect(edge.isSetupAssignment, isSetupAssignment);
expect(edge.guards, guards);
if (codeReference != null) {
expect(edge.codeReference, codeReference);
return edge;
/// Asserts that no edge exists with a node matching [source] and a node
/// matching [destination].
/// [source] and [destination] are converted to [NodeMatcher] objects if they
/// aren't already. In practice this means that the caller can pass in either
/// a [NodeMatcher] or a [NullabilityNode].
void assertNoEdge(Object source, Object destination) {
var edges = getEdges(source, destination);
if (edges.isNotEmpty) {
fail('Expected no edge $source -> $destination, found $edges');
/// Asserts that a union-type edge exists between nodes [x] and [y].
/// [x] and [y] are converted to [NodeMatcher] objects if they aren't already.
/// In practice this means that the caller can pass in either a [NodeMatcher]
/// or a [NullabilityNode].
void assertUnion(Object x, Object y) {
var edges = getEdges(x, y);
for (var edge in edges) {
if (edge.isUnion) {
expect(edge.upstreamNodes, hasLength(1));
fail('Expected union between $x and $y, not found');
/// Gets a list of all edges whose source matches [source] and whose
/// destination matches [destination].
/// [source] and [destination] are converted to [NodeMatcher] objects if they
/// aren't already. In practice this means that the caller can pass in either
/// a [NodeMatcher] or a [NullabilityNode].
List<NullabilityEdge> getEdges(Object source, Object destination) {
var sourceMatcher = NodeMatcher(source);
var destinationMatcher = NodeMatcher(destination);
var result = <NullabilityEdge>[];
for (var edge in graph.getAllEdges()) {
if (sourceMatcher.matches(edge.sourceNode) &&
destinationMatcher.matches(edge.destinationNode)) {
return result;
/// Returns a [NodeMatcher] that matches any node in the given set.
NodeSetMatcher inSet(Set<NullabilityNode> nodes) => NodeSetMatcher(nodes);
/// Creates a [NodeMatcher] matching a substitution node whose inner and outer
/// nodes match [inner] and [outer].
/// [inner] and [outer] are converted to [NodeMatcher] objects if they aren't
/// already. In practice this means that the caller can pass in either a
/// [NodeMatcher] or a [NullabilityNode].
NodeMatcher substitutionNode(Object inner, Object outer) =>
_SubstitutionNodeMatcher(NodeMatcher(inner), NodeMatcher(outer));
/// Mock representation of constraint variables.
class InstrumentedVariables extends Variables {
final _conditionalDiscard = <AstNode, ConditionalDiscard>{};
final _decoratedExpressionTypes = <Expression, DecoratedType>{};
final _expressionChecks = <Expression, ExpressionChecksOrigin>{};
InstrumentedVariables(NullabilityGraph graph, TypeProvider typeProvider,
LineInfo Function(String) getLineInfo)
: super(graph, typeProvider, getLineInfo);
/// Gets the [ExpressionChecks] associated with the given [expression].
ExpressionChecksOrigin checkExpression(Expression expression) =>
/// Gets the [conditionalDiscard] associated with the given [expression].
ConditionalDiscard conditionalDiscard(AstNode node) =>
/// Gets the [DecoratedType] associated with the given [expression].
DecoratedType decoratedExpressionType(Expression expression) =>
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;
void recordExpressionChecks(
Source source, Expression expression, ExpressionChecksOrigin origin) {
super.recordExpressionChecks(source, expression, origin);
_expressionChecks[_normalizeExpression(expression)] = origin;
/// Unwraps any parentheses surrounding [expression].
Expression _normalizeExpression(Expression expression) {
while (expression is ParenthesizedExpression) {
expression = (expression as ParenthesizedExpression).expression;
return expression;
class MigrationVisitorTestBase extends AbstractSingleUnitTest with EdgeTester {
InstrumentedVariables variables;
final NullabilityGraphForTesting graph;
final decoratedTypeParameterBounds = DecoratedTypeParameterBounds();
MigrationVisitorTestBase() : this._(NullabilityGraphForTesting());
NullabilityNode get always => graph.always;
NullabilityNode get never => graph.never;
TypeProvider get typeProvider => testAnalysisResult.typeProvider;
TypeSystemImpl get typeSystem =>
testAnalysisResult.typeSystem as TypeSystemImpl;
Future<CompilationUnit> analyze(String code) async {
await resolveTestUnit(code);
variables = InstrumentedVariables(graph, typeProvider, getLineInfo);
variables, testSource, null, graph, typeProvider, getLineInfo));
return testUnit;
/// Gets the [DecoratedType] associated with the constructor declaration whose
/// name matches [search].
DecoratedType decoratedConstructorDeclaration(String search) => variables
Map<ClassElement, DecoratedType> decoratedDirectSupertypes(String name) {
return variables.decoratedDirectSupertypes(findElement.classOrMixin(name));
/// 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 method declaration whose
/// name matches [search].
DecoratedType decoratedMethodType(String search) => variables
/// Gets the [DecoratedType] associated with the type annotation whose text
/// is [text].
DecoratedType decoratedTypeAnnotation(String text) {
return variables.decoratedTypeAnnotation(
testSource, findNode.typeAnnotation(text));
/// Gets the [ConditionalDiscard] information associated with the collection
/// element whose text is [text].
ConditionalDiscard elementDiscard(String text) {
return variables.conditionalDiscard(findNode.collectionElement(text));
/// Returns a [Matcher] that matches a [CodeReference] pointing to the given
/// file [offset], with the given [function] name.
TypeMatcher<CodeReference> matchCodeRef(
{@required int offset, @required String function}) {
var location = testUnit.lineInfo.getLocation(offset);
return TypeMatcher<CodeReference>()
.having((cr) => cr.line, 'line', location.lineNumber)
.having((cr) => cr.column, 'column', location.columnNumber)
.having((cr) => cr.function, 'function', function);
void setUp() {
DecoratedTypeParameterBounds.current = decoratedTypeParameterBounds;
/// Gets the [ConditionalDiscard] information associated with the statement
/// whose text is [text].
ConditionalDiscard statementDiscard(String text) {
return variables.conditionalDiscard(findNode.statement(text));
void tearDown() {
DecoratedTypeParameterBounds.current = null;
/// Abstract base class representing a thing that can be matched against
/// nullability nodes.
abstract class NodeMatcher {
factory NodeMatcher(Object expectation) {
if (expectation is NodeMatcher) return expectation;
if (expectation is NullabilityNode) return _ExactNodeMatcher(expectation);
'Unclear how to match node expectation of type ${expectation.runtimeType}');
void matched(NullabilityNode node);
bool matches(NullabilityNode node);
/// A [NodeMatcher] that matches any node contained in the given set.
class NodeSetMatcher extends _RecordingNodeMatcher {
final Set<NullabilityNode> _targetSet;
bool matches(NullabilityNode node) => _targetSet.contains(node);
/// A [NodeMatcher] that matches exactly one node.
class _ExactNodeMatcher implements NodeMatcher {
final NullabilityNode _expectation;
void matched(NullabilityNode node) {}
bool matches(NullabilityNode node) => node == _expectation;
/// Base class for [NodeMatcher]s that remember which nodes were matched.
abstract class _RecordingNodeMatcher implements NodeMatcher {
final List<NullabilityNode> _matchingNodes = [];
NullabilityNode get matchingNode => _matchingNodes.single;
void matched(NullabilityNode node) {
/// A [NodeMatcher] that matches a substitution node with the given inner and
/// outer nodes.
class _SubstitutionNodeMatcher implements NodeMatcher {
final NodeMatcher inner;
final NodeMatcher outer;
_SubstitutionNodeMatcher(this.inner, this.outer);
void matched(NullabilityNode node) {
if (node is NullabilityNodeForSubstitution) {
} else {
throw StateError(
'matched should only be called on nodes for which matches returned '
bool matches(NullabilityNode node) {
return node is NullabilityNodeForSubstitution &&
inner.matches(node.innerNode) &&