| // Copyright (c) 2016, 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. |
| |
| library fasta.verifier; |
| |
| import 'dart:core' hide MapEntry; |
| |
| import 'package:_fe_analyzer_shared/src/messages/severity.dart' show Severity; |
| |
| import 'package:kernel/ast.dart'; |
| |
| import 'package:kernel/transformations/flags.dart' show TransformerFlag; |
| |
| import 'package:kernel/verifier.dart' show VerifyingVisitor; |
| |
| import '../compiler_context.dart' show CompilerContext; |
| |
| import '../fasta_codes.dart' |
| show |
| LocatedMessage, |
| messageVerificationErrorOriginContext, |
| noLength, |
| templateInternalProblemVerificationError; |
| |
| import '../type_inference/type_schema.dart' show UnknownType; |
| |
| import 'redirecting_factory_body.dart' |
| show RedirectingFactoryBody, getRedirectingFactoryBody; |
| |
| List<LocatedMessage> verifyComponent(Component component, |
| {bool isOutline, bool afterConst, bool skipPlatform: false}) { |
| FastaVerifyingVisitor verifier = |
| new FastaVerifyingVisitor(isOutline, afterConst, skipPlatform); |
| component.accept(verifier); |
| return verifier.errors; |
| } |
| |
| class FastaVerifyingVisitor extends VerifyingVisitor { |
| final List<LocatedMessage> errors = <LocatedMessage>[]; |
| |
| Uri fileUri; |
| final List<TreeNode> treeNodeStack = <TreeNode>[]; |
| final bool skipPlatform; |
| |
| FastaVerifyingVisitor(bool isOutline, bool afterConst, this.skipPlatform) |
| : super(isOutline: isOutline, afterConst: afterConst); |
| |
| /// Invoked by all visit methods if the visited node is a [TreeNode]. |
| void enterTreeNode(TreeNode node) { |
| treeNodeStack.add(node); |
| } |
| |
| /// Invoked by all visit methods if the visited node is a [TreeNode]. |
| void exitTreeNode(TreeNode node) { |
| if (treeNodeStack.isEmpty) { |
| throw new StateError("Attempting to exit tree node '${node}' " |
| "when the tree node stack is empty."); |
| } |
| if (!identical(treeNodeStack.last, node)) { |
| throw new StateError("Attempting to exit tree node '${node}' " |
| "when another node '${treeNodeStack.last}' is active."); |
| } |
| treeNodeStack.removeLast(); |
| } |
| |
| TreeNode getLastSeenTreeNode({bool withLocation = false}) { |
| assert(treeNodeStack.isNotEmpty); |
| for (int i = treeNodeStack.length - 1; i >= 0; --i) { |
| TreeNode node = treeNodeStack[i]; |
| if (withLocation && !_hasLocation(node)) continue; |
| return node; |
| } |
| return null; |
| } |
| |
| TreeNode getSameLibraryLastSeenTreeNode({bool withLocation = false}) { |
| if (treeNodeStack.isEmpty) return null; |
| if (currentLibrary == null || currentLibrary.fileUri == null) return null; |
| |
| for (int i = treeNodeStack.length - 1; i >= 0; --i) { |
| TreeNode node = treeNodeStack[i]; |
| if (withLocation && !_hasLocation(node)) continue; |
| if (node.location?.file != null && |
| node.location.file == currentLibrary.fileUri) { |
| return node; |
| } |
| } |
| return null; |
| } |
| |
| static bool _hasLocation(TreeNode node) { |
| return node.location != null && |
| node.location.file != null && |
| node.fileOffset != null && |
| node.fileOffset != -1; |
| } |
| |
| static bool _isInSameLibrary(Library library, TreeNode node) { |
| if (library == null) return false; |
| if (library.fileUri == null) return false; |
| if (node.location == null) return false; |
| if (node.location.file == null) return false; |
| |
| return library.fileUri == node.location.file; |
| } |
| |
| TreeNode get localContext { |
| TreeNode result = getSameLibraryLastSeenTreeNode(withLocation: true); |
| if (result == null && |
| currentClassOrMember != null && |
| _isInSameLibrary(currentLibrary, currentClassOrMember)) { |
| result = currentClassOrMember; |
| } |
| return result; |
| } |
| |
| TreeNode get remoteContext { |
| TreeNode result = getLastSeenTreeNode(withLocation: true); |
| if (result != null && _isInSameLibrary(currentLibrary, result)) { |
| result = null; |
| } |
| return result; |
| } |
| |
| Uri checkLocation(TreeNode node, String name, Uri fileUri) { |
| if (name == null || name.contains("#")) { |
| // TODO(ahe): Investigate if these checks can be enabled: |
| // if (node.fileUri != null && node is! Library) { |
| // problem(node, "A synthetic node shouldn't have a fileUri", |
| // context: node); |
| // } |
| // if (node.fileOffset != -1) { |
| // problem(node, "A synthetic node shouldn't have a fileOffset", |
| // context: node); |
| // } |
| return fileUri; |
| } else { |
| if (fileUri == null) { |
| problem(node, "'$name' has no fileUri", context: node); |
| return fileUri; |
| } |
| if (node.fileOffset == -1 && node is! Library) { |
| problem(node, "'$name' has no fileOffset", context: node); |
| } |
| return fileUri; |
| } |
| } |
| |
| void checkSuperInvocation(TreeNode node) { |
| Member containingMember = getContainingMember(node); |
| if (containingMember == null) { |
| problem(node, 'Super call outside of any member'); |
| } else { |
| if (containingMember.transformerFlags & TransformerFlag.superCalls == 0) { |
| problem( |
| node, 'Super call in a member lacking TransformerFlag.superCalls'); |
| } |
| } |
| } |
| |
| Member getContainingMember(TreeNode node) { |
| while (node != null) { |
| if (node is Member) return node; |
| node = node.parent; |
| } |
| return null; |
| } |
| |
| @override |
| problem(TreeNode node, String details, {TreeNode context, TreeNode origin}) { |
| node ??= (context ?? currentClassOrMember); |
| int offset = node?.fileOffset ?? -1; |
| Uri file = node?.location?.file ?? fileUri; |
| Uri uri = file == null ? null : file; |
| LocatedMessage message = templateInternalProblemVerificationError |
| .withArguments(details) |
| .withLocation(uri, offset, noLength); |
| List<LocatedMessage> contextMessages; |
| if (origin != null) { |
| contextMessages = [ |
| messageVerificationErrorOriginContext.withLocation( |
| origin.location.file, origin.fileOffset, noLength) |
| ]; |
| } |
| CompilerContext.current |
| .report(message, Severity.error, context: contextMessages); |
| errors.add(message); |
| } |
| |
| @override |
| void visitAsExpression(AsExpression node) { |
| enterTreeNode(node); |
| super.visitAsExpression(node); |
| if (node.fileOffset == -1) { |
| TreeNode parent = node.parent; |
| while (parent != null) { |
| if (parent.fileOffset != -1) break; |
| parent = parent.parent; |
| } |
| problem(parent, "No offset for $node", context: node); |
| } |
| exitTreeNode(node); |
| } |
| |
| @override |
| void visitExpressionStatement(ExpressionStatement node) { |
| // Bypass verification of the [StaticGet] in [RedirectingFactoryBody] as |
| // this is a static get without a getter. |
| if (node is! RedirectingFactoryBody) { |
| enterTreeNode(node); |
| super.visitExpressionStatement(node); |
| exitTreeNode(node); |
| } |
| } |
| |
| @override |
| void visitLibrary(Library node) { |
| // Issue(http://dartbug.com/32530) |
| if (skipPlatform && node.importUri.scheme == 'dart') { |
| return; |
| } |
| |
| enterTreeNode(node); |
| fileUri = checkLocation(node, node.name, node.fileUri); |
| currentLibrary = node; |
| super.visitLibrary(node); |
| currentLibrary = null; |
| exitTreeNode(node); |
| } |
| |
| @override |
| void visitClass(Class node) { |
| enterTreeNode(node); |
| fileUri = checkLocation(node, node.name, node.fileUri); |
| super.visitClass(node); |
| exitTreeNode(node); |
| } |
| |
| @override |
| void visitField(Field node) { |
| enterTreeNode(node); |
| fileUri = checkLocation(node, node.name.text, node.fileUri); |
| super.visitField(node); |
| exitTreeNode(node); |
| } |
| |
| @override |
| void visitProcedure(Procedure node) { |
| enterTreeNode(node); |
| fileUri = checkLocation(node, node.name.text, node.fileUri); |
| super.visitProcedure(node); |
| exitTreeNode(node); |
| } |
| |
| bool isNullType(DartType node) { |
| if (node is InterfaceType) { |
| Uri importUri = node.classNode.enclosingLibrary.importUri; |
| return node.classNode.name == "Null" && |
| importUri.scheme == "dart" && |
| importUri.path == "core"; |
| } |
| return false; |
| } |
| |
| bool isObjectClass(Class c) { |
| return c.name == "Object" && |
| c.enclosingLibrary.importUri.scheme == "dart" && |
| c.enclosingLibrary.importUri.path == "core"; |
| } |
| |
| bool isTopType(DartType node) { |
| return node is DynamicType || |
| node is VoidType || |
| node is InterfaceType && |
| isObjectClass(node.classNode) && |
| (node.nullability == Nullability.nullable || |
| node.nullability == Nullability.legacy) || |
| node is FutureOrType && isTopType(node.typeArgument); |
| } |
| |
| bool isFutureOrNull(DartType node) { |
| return isNullType(node) || |
| node is FutureOrType && isFutureOrNull(node.typeArgument); |
| } |
| |
| @override |
| void defaultDartType(DartType node) { |
| final TreeNode localContext = this.localContext; |
| final TreeNode remoteContext = this.remoteContext; |
| |
| if (node is UnknownType) { |
| problem(localContext, "Unexpected appearance of the unknown type.", |
| origin: remoteContext); |
| } |
| |
| bool isTypeCast = localContext != null && |
| localContext is AsExpression && |
| localContext.isTypeError; |
| // Don't check cases like foo(x as{TypeError} T). In cases where foo comes |
| // from a library with a different opt-in status than the current library, |
| // the check may not be necessary. For now, just skip all type-error casts. |
| // TODO(dmitryas): Implement a more precise analysis. |
| bool isFromAnotherLibrary = remoteContext != null || isTypeCast; |
| |
| // Checking for non-legacy types in opt-out libraries. |
| { |
| bool neverLegacy = isNullType(node) || |
| isFutureOrNull(node) || |
| isTopType(node) || |
| node is InvalidType || |
| node is NeverType || |
| node is BottomType; |
| // TODO(dmitryas): Consider checking types coming from other libraries. |
| bool expectedLegacy = !isFromAnotherLibrary && |
| !currentLibrary.isNonNullableByDefault && |
| !neverLegacy; |
| if (expectedLegacy && node.nullability != Nullability.legacy) { |
| problem(localContext, |
| "Found a non-legacy type '${node}' in an opted-out library.", |
| origin: remoteContext); |
| } |
| } |
| |
| // Checking for legacy types in opt-in libraries. |
| { |
| Nullability nodeNullability = |
| node is InvalidType ? Nullability.undetermined : node.nullability; |
| // TODO(dmitryas): Consider checking types coming from other libraries. |
| if (!isFromAnotherLibrary && |
| currentLibrary.isNonNullableByDefault && |
| nodeNullability == Nullability.legacy) { |
| problem(localContext, |
| "Found a legacy type '${node}' in an opted-in library.", |
| origin: remoteContext); |
| } |
| } |
| |
| super.defaultDartType(node); |
| } |
| |
| @override |
| void visitFunctionType(FunctionType node) { |
| if (node.typeParameters.isNotEmpty) { |
| for (TypeParameter typeParameter in node.typeParameters) { |
| if (typeParameter.parent != null) { |
| problem( |
| localContext, |
| "Type parameters of function types shouldn't have parents: " |
| "$node."); |
| } |
| } |
| } |
| super.visitFunctionType(node); |
| } |
| |
| @override |
| void visitInterfaceType(InterfaceType node) { |
| if (isNullType(node) && node.nullability != Nullability.nullable) { |
| problem(localContext, "Found a not nullable Null type: ${node}"); |
| } |
| super.visitInterfaceType(node); |
| } |
| |
| @override |
| void visitSuperMethodInvocation(SuperMethodInvocation node) { |
| enterTreeNode(node); |
| checkSuperInvocation(node); |
| super.visitSuperMethodInvocation(node); |
| exitTreeNode(node); |
| } |
| |
| @override |
| void visitSuperPropertyGet(SuperPropertyGet node) { |
| enterTreeNode(node); |
| checkSuperInvocation(node); |
| super.visitSuperPropertyGet(node); |
| exitTreeNode(node); |
| } |
| |
| @override |
| void visitSuperPropertySet(SuperPropertySet node) { |
| enterTreeNode(node); |
| checkSuperInvocation(node); |
| super.visitSuperPropertySet(node); |
| exitTreeNode(node); |
| } |
| |
| @override |
| void visitStaticInvocation(StaticInvocation node) { |
| enterTreeNode(node); |
| super.visitStaticInvocation(node); |
| RedirectingFactoryBody body = getRedirectingFactoryBody(node.target); |
| if (body != null) { |
| problem(node, "Attempt to invoke redirecting factory."); |
| } |
| exitTreeNode(node); |
| } |
| |
| @override |
| void defaultTreeNode(TreeNode node) { |
| enterTreeNode(node); |
| super.defaultTreeNode(node); |
| exitTreeNode(node); |
| } |
| } |