| Index: pkg/analyzer/test/src/task/strong/front_end_runtime_check_test.dart
|
| diff --git a/pkg/analyzer/test/src/task/strong/front_end_runtime_check_test.dart b/pkg/analyzer/test/src/task/strong/front_end_runtime_check_test.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..8ca85e62df2f2548a7be15529c863ca5f4ebe831
|
| --- /dev/null
|
| +++ b/pkg/analyzer/test/src/task/strong/front_end_runtime_check_test.dart
|
| @@ -0,0 +1,438 @@
|
| +// Copyright (c) 2017, 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/ast/visitor.dart';
|
| +import 'package:analyzer/dart/element/element.dart';
|
| +import 'package:analyzer/dart/element/type.dart';
|
| +import 'package:analyzer/src/dart/element/type.dart';
|
| +import 'package:analyzer/src/generated/resolver.dart';
|
| +import 'package:analyzer/src/generated/type_system.dart';
|
| +import 'package:analyzer/src/generated/utilities_dart.dart';
|
| +import 'package:analyzer/src/task/strong/ast_properties.dart';
|
| +import 'package:front_end/src/base/instrumentation.dart' as fasta;
|
| +import 'package:front_end/src/fasta/compiler_context.dart' as fasta;
|
| +import 'package:front_end/src/fasta/testing/validating_instrumentation.dart'
|
| + as fasta;
|
| +import 'package:kernel/kernel.dart' as fasta;
|
| +import 'package:test/test.dart';
|
| +import 'package:test_reflective_loader/test_reflective_loader.dart';
|
| +
|
| +import 'front_end_test_common.dart';
|
| +
|
| +main() {
|
| + // Use a group() wrapper to specify the timeout.
|
| + group('front_end_runtime_check_test', () {
|
| + defineReflectiveSuite(() {
|
| + defineReflectiveTests(RunFrontEndRuntimeCheckTest);
|
| + });
|
| + }, timeout: new Timeout(const Duration(seconds: 120)));
|
| +}
|
| +
|
| +@reflectiveTest
|
| +class RunFrontEndRuntimeCheckTest extends RunFrontEndTest {
|
| + @override
|
| + get testSubdir => 'runtime_checks';
|
| +
|
| + @override
|
| + void visitUnit(TypeProvider typeProvider, CompilationUnit unit,
|
| + fasta.ValidatingInstrumentation validation, Uri uri) {
|
| + unit.accept(new _InstrumentationVisitor(
|
| + new StrongTypeSystemImpl(typeProvider), validation, uri));
|
| + }
|
| +}
|
| +
|
| +/// Visitor for ASTs that reports instrumentation for strong mode runtime
|
| +/// checks.
|
| +///
|
| +/// Note: this visitor doesn't attempt to report all runtime checks inserted by
|
| +/// analyzer; just the ones necessary to validate the front end tests. This
|
| +/// visitor might need to be updated as more front end tests are added.
|
| +class _InstrumentationVisitor extends GeneralizingAstVisitor<Null> {
|
| + final fasta.Instrumentation _instrumentation;
|
| + final Uri uri;
|
| + final StrongTypeSystemImpl _typeSystem;
|
| + final _elementNamer = new ElementNamer(null);
|
| +
|
| + _InstrumentationVisitor(this._typeSystem, this._instrumentation, this.uri);
|
| +
|
| + @override
|
| + visitAssignmentExpression(AssignmentExpression node) {
|
| + super.visitAssignmentExpression(node);
|
| + var leftHandSide = node.leftHandSide;
|
| + if (leftHandSide is PrefixedIdentifier) {
|
| + var staticElement = leftHandSide.identifier.staticElement;
|
| + if (staticElement is PropertyAccessorElement && staticElement.isSetter) {
|
| + var target = leftHandSide.prefix;
|
| + _annotateCheckCall(
|
| + staticElement,
|
| + target is ThisExpression,
|
| + isDynamicInvoke(leftHandSide.identifier),
|
| + target.staticType,
|
| + [],
|
| + [node.rightHandSide],
|
| + leftHandSide.identifier.offset);
|
| + }
|
| + }
|
| + }
|
| +
|
| + @override
|
| + visitClassDeclaration(ClassDeclaration node) {
|
| + super.visitClassDeclaration(node);
|
| + _emitForwardingStubs(node, node.name.offset);
|
| + }
|
| +
|
| + @override
|
| + visitClassTypeAlias(ClassTypeAlias node) {
|
| + super.visitClassTypeAlias(node);
|
| + _emitForwardingStubs(node, node.name.offset);
|
| + }
|
| +
|
| + @override
|
| + visitFormalParameter(FormalParameter node) {
|
| + super.visitFormalParameter(node);
|
| + if (node is DefaultFormalParameter) {
|
| + // Already handled via the contained parameter ast object
|
| + return;
|
| + }
|
| + _annotateFormalParameter(node.element, node.identifier.offset,
|
| + node.getAncestor((n) => n is ClassDeclaration));
|
| + }
|
| +
|
| + @override
|
| + visitMethodInvocation(MethodInvocation node) {
|
| + super.visitMethodInvocation(node);
|
| + var staticElement = node.methodName.staticElement;
|
| + var target = node.target;
|
| + var isThis = target is ThisExpression || target == null;
|
| + if (staticElement is PropertyAccessorElement) {
|
| + // Method invocation resolves to a getter; treat it as a get followed by a
|
| + // function invocation.
|
| + _annotateCheckReturn(
|
| + getImplicitOperationCast(node), node.methodName.offset);
|
| + _annotateCheckCall(
|
| + null,
|
| + isThis,
|
| + isDynamicInvoke(node.methodName),
|
| + null,
|
| + node.typeArguments?.arguments,
|
| + node.argumentList.arguments,
|
| + node.argumentList.offset);
|
| + } else {
|
| + _annotateCheckReturn(getImplicitCast(node), node.argumentList.offset);
|
| + _annotateCheckCall(
|
| + staticElement,
|
| + isThis,
|
| + isDynamicInvoke(node.methodName),
|
| + target?.staticType,
|
| + node.typeArguments?.arguments,
|
| + node.argumentList.arguments,
|
| + node.argumentList.offset);
|
| + }
|
| + }
|
| +
|
| + @override
|
| + visitPrefixedIdentifier(PrefixedIdentifier node) {
|
| + super.visitPrefixedIdentifier(node);
|
| + if (node.identifier.staticElement is MethodElement) {
|
| + _annotateTearOff(node, node.identifier.offset);
|
| + }
|
| + }
|
| +
|
| + @override
|
| + visitTypeParameter(TypeParameter node) {
|
| + super.visitTypeParameter(node);
|
| + if (node.parent.parent is MethodDeclaration) {
|
| + _annotateFormalParameter(node.element, node.name.offset,
|
| + node.getAncestor((n) => n is ClassDeclaration));
|
| + }
|
| + }
|
| +
|
| + @override
|
| + visitVariableDeclaration(VariableDeclaration node) {
|
| + super.visitVariableDeclaration(node);
|
| + if (node.parent.parent is FieldDeclaration) {
|
| + FieldElement element = node.element;
|
| + if (!element.isFinal) {
|
| + var setter = element.setter;
|
| + _annotateFormalParameter(setter.parameters[0], node.name.offset,
|
| + node.getAncestor((n) => n is ClassDeclaration));
|
| + }
|
| + }
|
| + }
|
| +
|
| + /// Generates the appropriate `@checkCall` annotation (if any) for a call
|
| + /// site.
|
| + ///
|
| + /// An annotation of `@checkCall=dynamic` indicates that the call is dynamic
|
| + /// (so it will have to be fully type checked). An annotation of
|
| + /// "@checkCall=interface(args)" indicates that the call statically resolves
|
| + /// to a member of an interface, but some of the arguments are "semi-typed" so
|
| + /// they may have to be type checked. `args` lists the positional indices of
|
| + /// the semi-typed arguments (counting from 0). If any type parameters need
|
| + /// to be checked, they are also listed by index, enclosed in `<>`. For
|
| + /// example, `@checkCall=interface(<0>,1)` means that type parameter 0 and
|
| + /// regular parameter 1 are semi-typed.
|
| + ///
|
| + /// [staticElement] is the element being invoked, or `null` if there is no
|
| + /// static element (either because this is a dynamic invocation or because the
|
| + /// thing being invoked is function-typed).
|
| + ///
|
| + /// [isThis] indicates whether the receiver of the invocation is `this`.
|
| + ///
|
| + /// [isDynamic] indicates whether analyzer has classified this invocation as a
|
| + /// dynamic invocation.
|
| + ///
|
| + /// [targetType] is the type of the target of the invocation, or `null` if
|
| + /// there is no target (e.g. because of implicit `this` or because the thing
|
| + /// being invoked is function-typed).
|
| + ///
|
| + /// [typeArguments] and [arguments] are the type arguments and regular
|
| + /// arguments of the invocation, respectively.
|
| + ///
|
| + /// [offset] is the location of the invocation in source code.
|
| + void _annotateCheckCall(
|
| + Element staticElement,
|
| + bool isThis,
|
| + bool isDynamic,
|
| + DartType targetType,
|
| + List<TypeAnnotation> typeArguments,
|
| + List<Expression> arguments,
|
| + int offset) {
|
| + if (staticElement is FunctionElement &&
|
| + staticElement.enclosingElement is CompilationUnitElement) {
|
| + // Invocation of a top level function; no annotation needed.
|
| + return;
|
| + }
|
| + if (isDynamic) {
|
| + if (targetType == null &&
|
| + staticElement != null &&
|
| + staticElement is! MethodElement) {
|
| + // Sometimes analyzer annotates invocations of function objects as
|
| + // dynamic (presumably due to "dynamic is bottom" behavior). Ignore
|
| + // this.
|
| + } else {
|
| + _recordCheckCall(offset, 'dynamic');
|
| + return;
|
| + }
|
| + }
|
| + if (staticElement is MethodElement && isThis) {
|
| + // Calls through "this" are always typed because the type parameters match
|
| + // up perfectly; no annotation needed.
|
| + return;
|
| + }
|
| + var semiTypedArgs = <String>[];
|
| + if (typeArguments != null) {
|
| + for (int argPosition = 0;
|
| + argPosition < typeArguments.length;
|
| + argPosition++) {
|
| + DartType getArgument(FunctionType functionType) {
|
| + return functionType.typeFormals[argPosition].bound;
|
| + }
|
| +
|
| + if (_isArgumentSemiTyped(targetType, staticElement, getArgument)) {
|
| + semiTypedArgs.add('<$argPosition>');
|
| + }
|
| + }
|
| + }
|
| + int argPosition = 0;
|
| + for (var argument in arguments) {
|
| + assert(argument is! NamedExpression); // TODO(paulberry): handle this
|
| + DartType getArgument(FunctionType functionType) {
|
| + // TODO(paulberry): handle named parameters
|
| + if (argPosition >= functionType.normalParameterTypes.length) {
|
| + return functionType.optionalParameterTypes[
|
| + argPosition - functionType.normalParameterTypes.length];
|
| + } else {
|
| + return functionType.normalParameterTypes[argPosition];
|
| + }
|
| + }
|
| +
|
| + if (_isArgumentSemiTyped(targetType, staticElement, getArgument)) {
|
| + semiTypedArgs.add('$argPosition');
|
| + }
|
| + ++argPosition;
|
| + }
|
| + if (semiTypedArgs.isEmpty) {
|
| + // We don't annotate invocations where all arguments are typed because
|
| + // that's the common case.
|
| + } else {
|
| + _recordCheckCall(
|
| + offset, 'interface(semiTyped:${semiTypedArgs.join(',')})');
|
| + }
|
| + }
|
| +
|
| + /// Generates the appropriate `@checkReturn` annotation (if any) for a call
|
| + /// site.
|
| + ///
|
| + /// An annotation of `@checkReturn=type` indicates that the value returned by
|
| + /// the call will have to be checked to make sure it is an instance of the
|
| + /// given type.
|
| + void _annotateCheckReturn(DartType castType, int offset) {
|
| + if (castType != null) {
|
| + _recordCheckReturn(offset, castType);
|
| + }
|
| + }
|
| +
|
| + /// Generates the appropriate `@checkFormal` annotation (if any) for a method
|
| + /// formal parameter, method type parameter, or field declaration.
|
| + ///
|
| + /// When this annotation is generated for a field declaration, it implicitly
|
| + /// refers to the value parameter of the synthetic setter.
|
| + ///
|
| + /// An annotation of `@checkFormal=unsafe` indicates that the parameter needs
|
| + /// to be type checked regardless of the call site.
|
| + ///
|
| + /// An annotation of `@checkFormal=semiSafe` indicates that the parameter
|
| + /// needs to be type checked when corresponding argument at the call site is
|
| + /// considered "semi-typed".
|
| + ///
|
| + /// No annotation indicates that the parameter only needs to be type checked
|
| + /// if the call site is a dynamic invocation.
|
| + void _annotateFormalParameter(
|
| + Element element, int offset, ClassDeclaration cls) {
|
| + if (element is ParameterElement && element.isCovariant) {
|
| + _recordCheckFormal(offset, 'unsafe');
|
| + } else if (cls != null) {
|
| + var covariantParams = getClassCovariantParameters(cls);
|
| + if (covariantParams != null && covariantParams.contains(element)) {
|
| + _recordCheckFormal(offset, 'semiSafe');
|
| + }
|
| + }
|
| + }
|
| +
|
| + /// Generates the appropriate `@checkTearOff` annotation (if any) for a call
|
| + /// site.
|
| + ///
|
| + /// An annotation of `@checkTearOff=type` indicates that the torn off function
|
| + /// will have to be checked to make sure it is an instance of the given type.
|
| + void _annotateTearOff(Expression node, int offset) {
|
| + // TODO(paulberry): handle dynamic tear offs
|
| + // Note: we don't annotate that non-dynamic tear offs use "interface"
|
| + // dispatch because that's the common case.
|
| + var castType = getImplicitCast(node);
|
| + if (castType != null) {
|
| + _recordCheckTearOff(offset, castType);
|
| + }
|
| + }
|
| +
|
| + /// Generates the appropriate `@forwardingStub` annotation (if any) for a
|
| + /// class declaration or mixin application.
|
| + ///
|
| + /// An annotation of `@forwardingStub=rettype name(args)` indicates that a
|
| + /// forwarding stub must be inserted into the class having the given name and
|
| + /// return type. Each argument is listed in `args` as `safety type name`,
|
| + /// where safety is one of `safe` or `semiSafe`.
|
| + void _emitForwardingStubs(Declaration node, int offset) {
|
| + var covariantParams = getSuperclassCovariantParameters(node);
|
| + if (covariantParams != null && covariantParams.isNotEmpty) {
|
| + for (var member
|
| + in covariantParams.map((p) => p.enclosingElement).toSet()) {
|
| + var memberName = member.name;
|
| + if (member is PropertyAccessorElement) {
|
| + throw new UnimplementedError(); // TODO(paulberry)
|
| + } else if (member is MethodElement) {
|
| + var paramDescrs = <String>[];
|
| + for (var param in member.parameters) {
|
| + // TODO(paulberry): test the safe case
|
| + var safetyDescr =
|
| + covariantParams.contains(param) ? 'semiSafe' : 'safe';
|
| + var typeDescr = _typeToString(param.type);
|
| + var paramName = param.name;
|
| + // TODO(paulberry): if necessary, support other parameter kinds
|
| + assert(param.parameterKind == ParameterKind.REQUIRED);
|
| + paramDescrs.add('$safetyDescr $typeDescr $paramName');
|
| + }
|
| + var returnTypeDescr = _typeToString(member.returnType);
|
| + var stub = '$returnTypeDescr $memberName(${paramDescrs.join(', ')})';
|
| + _recordForwardingStub(offset, stub);
|
| + } else {
|
| + throw new StateError('Unexpected covariant member $member');
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + /// Determines whether an argument at a call site should be considered
|
| + /// "semi-typed".
|
| + ///
|
| + /// [targetType] indicates the type of the interface being invoked.
|
| + ///
|
| + /// [invocationTarget] is the method or getter/setter being invoked.
|
| + ///
|
| + /// [getArgument] is a callback for accessing the corresponding argument type
|
| + /// from a [FunctionType].
|
| + bool _isArgumentSemiTyped(InterfaceType targetType, Element invocationTarget,
|
| + DartType getArgument(FunctionType functionType)) {
|
| + bool _checkTypes(DartType originalArgumentType,
|
| + DartType lookupArgumentType(InterfaceType interfaceType)) {
|
| + // If the target type lacks type parameters, then everything is safe.
|
| + if (targetType.typeParameters.isEmpty) return false;
|
| +
|
| + // To see if this argument needs to be semi-typed, we try substituting
|
| + // bottom in for all the active type parameters. If the resulting
|
| + // argument static type is a supertype of its current static type, then
|
| + // that means that regardless of what we pass in, it won't fail a type
|
| + // check.
|
| + var substitutedInterfaceType = targetType.element.type.instantiate(
|
| + new List<DartType>.filled(
|
| + targetType.typeParameters.length, BottomTypeImpl.instance));
|
| + var substitutedArgumentType =
|
| + lookupArgumentType(substitutedInterfaceType);
|
| + return !_typeSystem.isSubtypeOf(
|
| + originalArgumentType, substitutedArgumentType);
|
| + }
|
| +
|
| + if (invocationTarget is LocalVariableElement || invocationTarget == null) {
|
| + // This is an invocation of a closure, so every argument is semi-typed.
|
| + return true;
|
| + } else if (invocationTarget is PropertyAccessorElement &&
|
| + invocationTarget.isSetter) {
|
| + return _checkTypes(
|
| + invocationTarget.parameters[0].type,
|
| + (InterfaceType type) => type
|
| + .lookUpSetter(invocationTarget.name, invocationTarget.library)
|
| + .parameters[0]
|
| + .type);
|
| + } else if (invocationTarget is MethodElement) {
|
| + return _checkTypes(
|
| + getArgument(invocationTarget.type),
|
| + (InterfaceType type) => getArgument(type
|
| + .lookUpMethod(invocationTarget.name, invocationTarget.library)
|
| + .type));
|
| + } else {
|
| + throw new UnimplementedError(
|
| + 'Unexpected invocation target type: ${invocationTarget.runtimeType}');
|
| + }
|
| + }
|
| +
|
| + void _recordCheckCall(int offset, String safety) {
|
| + _instrumentation.record(uri, offset, 'checkCall',
|
| + new fasta.InstrumentationValueLiteral(safety));
|
| + }
|
| +
|
| + void _recordCheckFormal(int offset, String safety) {
|
| + _instrumentation.record(uri, offset, 'checkFormal',
|
| + new fasta.InstrumentationValueLiteral(safety));
|
| + }
|
| +
|
| + void _recordCheckReturn(int offset, DartType castType) {
|
| + _instrumentation.record(uri, offset, 'checkReturn',
|
| + new InstrumentationValueForType(castType, _elementNamer));
|
| + }
|
| +
|
| + void _recordCheckTearOff(int offset, DartType castType) {
|
| + _instrumentation.record(uri, offset, 'checkTearOff',
|
| + new InstrumentationValueForType(castType, _elementNamer));
|
| + }
|
| +
|
| + void _recordForwardingStub(int offset, String descr) {
|
| + _instrumentation.record(uri, offset, 'forwardingStub',
|
| + new fasta.InstrumentationValueLiteral(descr));
|
| + }
|
| +
|
| + String _typeToString(DartType type) {
|
| + return new InstrumentationValueForType(type, _elementNamer).toString();
|
| + }
|
| +}
|
|
|