blob: 3f57caae3e03af83e681128bf80f472d29233131 [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: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 {
@override
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))
.toList();
parameters.addAll(positional.map((t) => ParameterElementImpl.synthetic(
'p${i++}', t.type!, ParameterKind.POSITIONAL)));
parameters.addAll(named.entries.map((e) => ParameterElementImpl.synthetic(
e.key, e.value.type!, ParameterKind.NAMED)));
return DecoratedType(
FunctionTypeImpl(
typeFormals: typeFormals,
parameters: parameters,
returnType: returnType.type!,
nullabilitySuffix: NullabilitySuffix.star,
),
node ?? newNode(),
returnType: returnType,
positionalParameters: required.toList()..addAll(positional),
namedParameters: named);
}
DecoratedType future(DecoratedType parameter, {NullabilityNode? node}) {
return DecoratedType(
typeProvider.futureType(parameter.type!), node ?? newNode(),
typeArguments: [parameter]);
}
DecoratedType futureOr(DecoratedType parameter, {NullabilityNode? node}) {
return DecoratedType(
typeProvider.futureOrType(parameter.type!), node ?? newNode(),
typeArguments: [parameter]);
}
DecoratedType int_({NullabilityNode? node}) =>
DecoratedType(typeProvider.intType, node ?? newNode());
DecoratedType iterable(DecoratedType elementType, {NullabilityNode? node}) =>
DecoratedType(
typeProvider.iterableType(elementType.type!), node ?? newNode(),
typeArguments: [elementType]);
DecoratedType list(DecoratedType elementType, {NullabilityNode? node}) =>
DecoratedType(typeProvider.listType(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(
typeParameter.instantiate(
nullabilitySuffix: NullabilitySuffix.star,
),
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.
@override
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,
unit.declaredElement!.library));
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) {
result.add(edge.destinationNode);
}
}
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)) {
pending.add(edge.sourceNode);
}
}
}
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));
return;
}
}
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)) {
sourceMatcher.matched(edge.sourceNode);
destinationMatcher.matched(edge.destinationNode);
result.add(edge);
}
}
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)
: super(graph, typeProvider);
/// Gets the [ExpressionChecks] associated with the given [expression].
ExpressionChecksOrigin? 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)];
@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, 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.expression;
}
return expression;
}
}
class MigrationVisitorTestBase extends AbstractSingleUnitTest with EdgeTester {
InstrumentedVariables? variables;
final NullabilityGraphForTesting graph;
final decoratedTypeParameterBounds = DecoratedTypeParameterBounds();
MigrationVisitorTestBase() : this._(NullabilityGraphForTesting());
MigrationVisitorTestBase._(this.graph);
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);
testUnit!
.accept(NodeBuilder(variables, testSource, null, graph, typeProvider));
return testUnit!;
}
/// Gets the [DecoratedType] associated with the constructor declaration whose
/// name matches [search].
DecoratedType decoratedConstructorDeclaration(String search) => variables!
.decoratedElementType(findNode.constructor(search).declaredElement!);
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!.decoratedElementType(
findNode.methodDeclaration(search).declaredElement!);
/// 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;
super.setUp();
}
/// 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;
super.tearDown();
}
}
/// 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);
fail(
'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;
NodeSetMatcher(this._targetSet);
@override
bool matches(NullabilityNode? node) => _targetSet.contains(node);
}
/// A [NodeMatcher] that matches exactly one node.
class _ExactNodeMatcher implements NodeMatcher {
final NullabilityNode _expectation;
_ExactNodeMatcher(this._expectation);
@override
void matched(NullabilityNode? node) {}
@override
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;
@override
void matched(NullabilityNode? node) {
_matchingNodes.add(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);
@override
void matched(NullabilityNode? node) {
if (node is NullabilityNodeForSubstitution) {
inner.matched(node.innerNode);
outer.matched(node.outerNode);
} else {
throw StateError(
'matched should only be called on nodes for which matches returned '
'true');
}
}
@override
bool matches(NullabilityNode? node) {
return node is NullabilityNodeForSubstitution &&
inner.matches(node.innerNode) &&
outer.matches(node.outerNode);
}
}