mirror of
https://github.com/dart-lang/sdk
synced 2024-11-02 09:43:08 +00:00
44316060f1
Change-Id: Ida7a9c2ac35943aed2d75f81f98ce7056b616aee Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/260741 Commit-Queue: Konstantin Shcheglov <scheglov@google.com> Reviewed-by: Samuel Rawlins <srawlins@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
516 lines
18 KiB
Dart
516 lines
18 KiB
Dart
// 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(super.graph, super.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 {
|
|
late 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<InterfaceElement, 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);
|
|
}
|
|
}
|