// 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 kernel.checks; import 'ast.dart'; import 'target/targets.dart'; import 'transformations/flags.dart'; import 'type_environment.dart' show StatefulStaticTypeContext, TypeEnvironment; /// Stages at which verification can occur. /// /// These can be used to enforce different invariants during different stages /// of the compilation. enum VerificationStage { /// Verification after the outline compilation. outline, /// Verification after the body, aka full, compilation, but before pre- /// constant evaluation transformations have been performed. beforePreConstantEvaluationTransformations, /// Verification after pre- constant evaluation transformations have been /// performed but before constant evaluation. beforeConstantEvaluation, /// Verification after constant evaluation but before modular transformations /// have been performed. afterConstantEvaluation, /// Verification after modular transformations have been performed. /// /// This is final stage of a normal compilation. afterModularTransformations, /// Verification after global transformations have been performed. /// /// The global transformation is an additional step performed by some /// backends which is not triggered by the front end compilation itself. afterGlobalTransformations, ; bool operator <(VerificationStage other) => index < other.index; bool operator <=(VerificationStage other) => index <= other.index; bool operator >(VerificationStage other) => index > other.index; bool operator >=(VerificationStage other) => index >= other.index; } /// Interface that defines how the AST is verified. class Verification { const Verification(); /// Returns `true` if [node] is allowed to have no file offset. bool allowNoFileOffset(VerificationStage stage, TreeNode node) { return node is Library; } /// Returns `true` if [node] is allowed to have location with a file offset /// that is not in the range of the enclosing file. bool allowInvalidLocation(VerificationStage stage, TreeNode node) { return false; } } void verifyComponent( Target target, VerificationStage stage, Component component, {bool skipPlatform = false, bool Function(Library library)? librarySkipFilter}) { VerifyingVisitor.check(target, stage, component, skipPlatform: skipPlatform, librarySkipFilter: librarySkipFilter); } class VerificationErrorListener { const VerificationErrorListener(); void reportError(String details, {required TreeNode? node, required Uri? problemUri, required int? problemOffset, required TreeNode? context, required TreeNode? origin}) { throw new VerificationError(context, node, details); } } class VerificationError { final TreeNode? context; final TreeNode? node; final String details; VerificationError(this.context, this.node, this.details); @override String toString() { Location? location; try { location = node?.location ?? context?.location; } catch (_) { // TODO(ahe): Fix the compiler instead. } if (location != null) { String file = location.file.toString(); return "$file:${location.line}:${location.column}: Verification error:" " $details"; } else { return "Verification error: $details\n" "Context: '$context'.\n" "Node: '$node'."; } } } enum TypedefState { Done, BeingChecked } /// Checks that a kernel component is well-formed. /// /// This does not include any kind of type checking. class VerifyingVisitor extends RecursiveResultVisitor { final Target target; Uri? fileUri; final VerificationErrorListener listener; final List treeNodeStack = []; final bool skipPlatform; final bool Function(Library library)? librarySkipFilter; final Set classes = new Set(); final Set typedefs = new Set(); Set typeParametersInScope = new Set(); Set structuralParametersInScope = new Set(); Set variableDeclarationsInScope = new Set(); final List variableStack = []; final Map typedefState = {}; final Set seenConstants = {}; Map? _extensionsMembers; Map? _extensionTypeMembers; bool classTypeParametersAreInScope = false; /// The compilation stage at which this verification is performed. final VerificationStage stage; AsyncMarker currentAsyncMarker = AsyncMarker.Sync; bool inCatchBlock = false; bool inUnevaluatedConstant = false; bool inConstant = false; Library? currentLibrary; Member? currentMember; Class? currentClass; Extension? currentExtension; ExtensionTypeDeclaration? currentExtensionTypeDeclaration; TreeNode? currentParent; TreeNode? get currentClassOrExtensionOrMember => currentMember ?? currentClass ?? currentExtension ?? currentExtensionTypeDeclaration; static void check(Target target, VerificationStage stage, Component component, {required bool skipPlatform, bool Function(Library library)? librarySkipFilter}) { component.accept(new VerifyingVisitor(target, stage, skipPlatform: skipPlatform, librarySkipFilter: librarySkipFilter)); } VerifyingVisitor(this.target, this.stage, {required this.skipPlatform, required this.librarySkipFilter, VerificationErrorListener this.listener = const VerificationErrorListener()}); /// If true, relax certain checks for *outline* mode. For example, don't /// attempt to validate constructor initializers. bool get isOutline => stage == VerificationStage.outline; /// If true, assume that constant evaluation has been performed (with a /// target that did not opt out of any of the constant inlining) and report /// a verification error for anything that should have been removed by it. bool get afterConst => stage >= VerificationStage.afterConstantEvaluation; /// If true, constant fields and local variables are expected to be inlined. bool get constantsAreAlwaysInlined => target.constantsBackend.alwaysInlineConstants; @override void defaultTreeNode(TreeNode node) { enterTreeNode(node); visitChildren(node); exitTreeNode(node); } @override void defaultConstantReference(Constant constant) { if (seenConstants.add(constant)) { constant.accept(this); } } @override void defaultConstant(Constant constant) { constant.visitChildren(this); } void problem(TreeNode? node, String details, {TreeNode? context, TreeNode? origin}) { TreeNode? problemNode = node ?? context ?? currentClassOrExtensionOrMember; int offset = problemNode?.fileOffset ?? -1; Location? location = problemNode != null ? _getLocation(problemNode, allowInvalidLocation: true) : null; Uri? file = location?.file ?? fileUri; Uri? uri = file == null ? null : file; String verifierState = 'Target=${target.name}, $stage: '; listener.reportError('$verifierState$details', problemUri: uri, problemOffset: offset, node: node, context: context ?? currentClassOrExtensionOrMember, origin: origin); } TreeNode? enterParent(TreeNode node) { if (!identical(node.parent, currentParent)) { problem( node, "Incorrect parent pointer on ${node}:" " expected ${currentParent}," " but found: ${node.parent}.", context: currentParent); } TreeNode? oldParent = currentParent; currentParent = node; return oldParent; } void exitParent(TreeNode? oldParent) { currentParent = oldParent; } int enterLocalScope() => variableStack.length; void exitLocalScope(int stackHeight) { for (int i = stackHeight; i < variableStack.length; ++i) { undeclareVariable(variableStack[i]); } variableStack.length = stackHeight; } /// Calls [f] with [node] set up as the parent node. void inTreeNode(TreeNode node, void Function() f) { TreeNode? oldParent = enterParent(node); f(); exitParent(oldParent); } void visitChildren(TreeNode node) { inTreeNode(node, () => node.visitChildren(this)); } void visitWithLocalScope(TreeNode node) { enterTreeNode(node); int stackHeight = enterLocalScope(); visitChildren(node); exitLocalScope(stackHeight); exitTreeNode(node); } void declareVariable(VariableDeclaration variable) { if (variableDeclarationsInScope.contains(variable)) { problem(variable, "Variable '$variable' declared more than once."); } variableDeclarationsInScope.add(variable); variableStack.add(variable); } void undeclareVariable(VariableDeclaration variable) { variableDeclarationsInScope.remove(variable); } void declareTypeParameters(List parameters) { for (int i = 0; i < parameters.length; ++i) { TypeParameter parameter = parameters[i]; if (identical(parameter.bound, TypeParameter.unsetBoundSentinel)) { problem( currentParent, "Missing bound for type parameter '$parameter'."); } if (identical( parameter.defaultType, TypeParameter.unsetDefaultTypeSentinel)) { problem(currentParent, "Missing default type for type parameter '$parameter'."); } if (!typeParametersInScope.add(parameter)) { problem(parameter, "Type parameter '$parameter' redeclared."); } } } void declareStructuralParameters(List parameters) { for (int i = 0; i < parameters.length; ++i) { StructuralParameter parameter = parameters[i]; if (identical(parameter.bound, StructuralParameter.unsetBoundSentinel)) { problem( currentParent, "Missing bound for type parameter '$parameter'."); } if (identical(parameter.defaultType, StructuralParameter.unsetDefaultTypeSentinel)) { problem(currentParent, "Missing default type for type parameter '$parameter'."); } if (!structuralParametersInScope.add(parameter)) { problem(currentParent, "Type parameter '$parameter' redeclared."); } } } void undeclareTypeParameters(List parameters) { typeParametersInScope.removeAll(parameters); } void undeclareStructuralParameters(List parameters) { structuralParametersInScope.removeAll(parameters); } void checkVariableInScope(VariableDeclaration variable, TreeNode where) { if (!variableDeclarationsInScope.contains(variable)) { problem(where, "Variable '$variable' used out of scope."); } } @override void visitComponent(Component component) { void declareMember(Member member) { if (member.transformerFlags & TransformerFlag.seenByVerifier != 0) { problem(member.function, "Member '$member' has been declared more than once."); } member.transformerFlags |= TransformerFlag.seenByVerifier; } void undeclareMember(Member member) { member.transformerFlags &= ~TransformerFlag.seenByVerifier; } try { for (Library library in component.libraries) { for (Class class_ in library.classes) { if (!classes.add(class_)) { problem(class_, "Class '$class_' declared more than once."); } } for (Typedef typedef_ in library.typedefs) { if (!typedefs.add(typedef_)) { problem(typedef_, "Typedef '$typedef_' declared more than once."); } } library.forEachMember(declareMember); for (Class class_ in library.classes) { class_.forEachMember(declareMember); } for (ExtensionTypeDeclaration extensionTypeDeclaration in library.extensionTypeDeclarations) { extensionTypeDeclaration.procedures.forEach(declareMember); } } visitChildren(component); } finally { for (Library library in component.libraries) { library.forEachMember(undeclareMember); for (Class class_ in library.classes) { class_.forEachMember(undeclareMember); } for (ExtensionTypeDeclaration extensionTypeDeclaration in library.extensionTypeDeclarations) { extensionTypeDeclaration.procedures.forEach(undeclareMember); } } variableStack.forEach(undeclareVariable); } } @override void visitLibrary(Library node) { if (skipPlatform && node.importUri.isScheme('dart') && // 'dart:test' is used in the unit tests and isn't an actual part of the // platform so we don't skip its verification. node.importUri.path != 'test') { return; } if (librarySkipFilter != null && librarySkipFilter!(node)) { return; } enterTreeNode(node); fileUri = checkLocation(node, node.name, node.fileUri); currentLibrary = node; super.visitLibrary(node); currentLibrary = null; exitTreeNode(node); _extensionsMembers = null; _extensionTypeMembers = null; } Map _computeExtensionMembers( Library library) { if (_extensionsMembers == null) { Map map = _extensionsMembers = {}; for (Extension extension in library.extensions) { for (ExtensionMemberDescriptor descriptor in extension.memberDescriptors) { Reference? memberReference = descriptor.memberReference; if (memberReference != null) { map[memberReference] = descriptor; Member member = memberReference.asMember; if (!member.isExtensionMember) { problem( member, "Member $member (${descriptor}) from $extension is not " " marked as an extension member."); } } Reference? tearOffReference = descriptor.tearOffReference; if (tearOffReference != null) { map[tearOffReference] = descriptor; Member tearOff = tearOffReference.asMember; if (!tearOff.isExtensionMember) { problem( tearOff, "Tear-off $tearOff (${descriptor}) from $extension is not " "marked as an extension member."); } } if (memberReference == null && tearOffReference == null) { problem( extension, "Both member and tear-off references are null in " "the descriptor $descriptor from $extension."); } } } } return _extensionsMembers!; } Map _computeExtensionTypeMembers( Library library) { if (_extensionTypeMembers == null) { Map map = _extensionTypeMembers = {}; for (ExtensionTypeDeclaration extensionTypeDeclaration in library.extensionTypeDeclarations) { for (ExtensionTypeMemberDescriptor descriptor in extensionTypeDeclaration.memberDescriptors) { Reference? memberReference = descriptor.memberReference; if (memberReference != null) { map[memberReference] = descriptor; Member member = memberReference.asMember; if (!member.isExtensionTypeMember) { problem( member, "Member $member (${descriptor}) from " "$extensionTypeDeclaration is not marked as an extension " "type member."); } } Reference? tearOffReference = descriptor.tearOffReference; if (tearOffReference != null) { map[tearOffReference] = descriptor; Member tearOff = tearOffReference.asMember; if (!tearOff.isExtensionTypeMember) { problem( tearOff, "Tear-off $tearOff (${descriptor}) from " "$extensionTypeDeclaration is not marked as an extension " "type member."); } } if (memberReference == null && tearOffReference == null) { problem( extensionTypeDeclaration, "Both member and tear-off references are null in " "the descriptor $descriptor from $extensionTypeDeclaration."); } } } } return _extensionTypeMembers!; } @override void visitExtension(Extension node) { enterTreeNode(node); fileUri = checkLocation(node, node.name, node.fileUri); currentExtension = node; _computeExtensionMembers(node.enclosingLibrary); declareTypeParameters(node.typeParameters); final TreeNode? oldParent = enterParent(node); node.visitChildren(this); exitParent(oldParent); undeclareTypeParameters(node.typeParameters); currentExtension = null; exitTreeNode(node); } @override void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) { enterTreeNode(node); fileUri = checkLocation(node, node.name, node.fileUri); currentExtensionTypeDeclaration = node; _computeExtensionTypeMembers(node.enclosingLibrary); declareTypeParameters(node.typeParameters); final TreeNode? oldParent = enterParent(node); for (DartType type in node.implements) { if (!(type is ExtensionType || type is InterfaceType)) { problem( node, "Extension type can only implement extension types and interface " "types. Found $type."); } else if (type is ExtensionType && type.nullability == Nullability.nullable || type is! ExtensionType && type.isPotentiallyNullable) { problem( node, "Extension type can only implement non-nullable types. " "Found $type."); } } node.visitChildren(this); exitParent(oldParent); undeclareTypeParameters(node.typeParameters); currentExtensionTypeDeclaration = null; exitTreeNode(node); } void checkTypedef(Typedef node) { TypedefState? state = typedefState[node]; if (state == TypedefState.Done) return; if (state == TypedefState.BeingChecked) { problem(node, "The typedef '$node' refers to itself", context: node); } assert(state == null); enterTreeNode(node); typedefState[node] = TypedefState.BeingChecked; Set savedTypeParameters = typeParametersInScope; typeParametersInScope = node.typeParameters.toSet(); TreeNode? savedParent = currentParent; currentParent = node; // Visit children without checking the parent pointer on the typedef itself // since this can be called from a context other than its true parent. node.visitChildren(this); currentParent = savedParent; typeParametersInScope = savedTypeParameters; typedefState[node] = TypedefState.Done; exitTreeNode(node); } @override void visitTypedef(Typedef node) { enterTreeNode(node); fileUri = checkLocation(node, node.name, node.fileUri); checkTypedef(node); // Enter and exit the node to check the parent pointer on the typedef node. exitParent(enterParent(node)); exitTreeNode(node); } void _findExtensionMember(Member node) { assert(node.isExtensionMember); Map extensionMembers = _computeExtensionMembers(node.enclosingLibrary); if (!extensionMembers.containsKey(node.reference)) { problem( node, "Extension member $node is not found in any extension of the " "enclosing library."); } } void _findExtensionTypeMember(Member node) { assert(node.isExtensionTypeMember); Map extensionTypeMembers = _computeExtensionTypeMembers(node.enclosingLibrary); if (node is Procedure && node.stubKind == ProcedureStubKind.RepresentationField) { if (extensionTypeMembers.containsKey(node.reference)) { problem( node, "Extension type representation field $node is found amongst the " "lowered extension type members of the enclosing library."); } } else { if (!extensionTypeMembers.containsKey(node.reference)) { problem( node, "Extension type member $node is not found in any extension type " "declaration of the enclosing library."); } } } @override void visitField(Field node) { enterTreeNode(node); fileUri = checkLocation(node, node.name.text, node.fileUri); currentMember = node; TreeNode? oldParent = enterParent(node); bool isTopLevel = node.parent == currentLibrary; if (isTopLevel && !node.isStatic) { problem(node, "The top-level field '${node.name.text}' should be static", context: node); } if (node.isConst && !node.isStatic) { problem(node, "The const field '${node.name.text}' should be static", context: node); } bool isImmutable = node.isLate ? (node.isFinal && node.initializer != null) : (node.isFinal || node.isConst); if (isImmutable == node.hasSetter) { if (node.hasSetter) { problem(node, "The immutable field '${node.name.text}' has a setter reference", context: node); } else { if (isOutline && node.isLate) { // TODO(johnniwinther): Should we add a flag on Field for having // a declared initializer? // The initializer is not included in the outline so we can't tell // whether it has an initializer or not. } else { problem(node, "The mutable field '${node.name.text}' has no setter reference", context: node); } } } if (node.isExtensionMember) { _findExtensionMember(node); } if (node.isExtensionTypeMember) { _findExtensionTypeMember(node); } classTypeParametersAreInScope = !node.isStatic; node.initializer?.accept(this); node.type.accept(this); classTypeParametersAreInScope = false; visitList(node.annotations, this); exitParent(oldParent); currentMember = null; exitTreeNode(node); } @override void visitProcedure(Procedure node) { enterTreeNode(node); fileUri = checkLocation(node, node.name.text, node.fileUri); if (node.isExtensionMember) { _findExtensionMember(node); } if (node.isExtensionTypeMember) { _findExtensionTypeMember(node); } if (node.isRedirectingFactory && node.function.redirectingFactoryTarget == null) { problem( node, "Procedure '${node.name}' doesn't have a redirecting " "factory target, but has the 'isRedirectingFactory' bit set."); } else if (!node.isRedirectingFactory && node.function.redirectingFactoryTarget != null) { problem( node, "Procedure '${node.name}' has redirecting factory target, but " "doesn't have the 'isRedirectingFactory' bit set."); } currentMember = node; TreeNode? oldParent = enterParent(node); classTypeParametersAreInScope = !node.isStatic; if (node.isAbstract && node.isExternal) { problem(node, "Procedure cannot be both abstract and external."); } if (node.isMemberSignature && node.isForwardingStub) { problem( node, "Procedure cannot be both a member signature and a forwarding stub: " "$node."); } if (node.isMemberSignature && node.isForwardingSemiStub) { problem( node, "Procedure cannot be both a member signature and a forwarding semi " "stub $node."); } if (node.isMemberSignature && node.isNoSuchMethodForwarder) { problem( node, "Procedure cannot be both a member signature and a noSuchMethod " "forwarder $node."); } if (node.isMemberSignature && node.memberSignatureOrigin == null) { problem( node, "Member signature must have a member signature origin $node."); } if (node.abstractForwardingStubTarget != null && !(node.isForwardingStub || node.isForwardingSemiStub)) { problem( node, "Only forwarding stubs can have a forwarding stub interface target " "$node."); } if (node.concreteForwardingStubTarget != null && !(node.isForwardingStub || node.isForwardingSemiStub)) { problem( node, "Only forwarding stubs can have a forwarding stub super target " "$node."); } node.function.accept(this); classTypeParametersAreInScope = false; visitList(node.annotations, this); exitParent(oldParent); // TODO(johnniwinther): Enable this invariant. Possibly by removing bodies // from external procedures declared with a body or by removing the external // flag from such procedures. /*if (node.isExternal) { if (node.function.body != null) { problem(node, "External procedure has non-null body."); } } else if (node.isAbstract) { if (node.function.body != null) { problem(node, "Abstract procedure has non-null body."); } } else { if (node.function.body == null) { problem(node, "Non-external/abstract procedure has no body."); } }*/ currentMember = null; exitTreeNode(node); } @override void visitConstructor(Constructor node) { enterTreeNode(node); fileUri = checkLocation(node, node.name.text, node.fileUri); currentMember = node; classTypeParametersAreInScope = true; if (node.isExtensionMember) { _findExtensionMember(node); } if (node.isExtensionTypeMember) { _findExtensionTypeMember(node); } // The constructor member needs special treatment due to parameters being // in scope in the initializer list. TreeNode? oldParent = enterParent(node); int stackHeight = enterLocalScope(); visitChildren(node.function); visitList(node.initializers, this); if (!isOutline) { checkInitializers(node); } exitLocalScope(stackHeight); classTypeParametersAreInScope = false; visitList(node.annotations, this); exitParent(oldParent); // TODO(johnniwinther): Enable this invariant. Possibly by removing bodies // from external constructors declared with a body or by removing the // external flag from such constructors. /*if (node.isExternal) { if (node.function.body != null) { problem(node, "External constructor has non-null body."); } } else { if (node.function.body == null) { problem(node, "Non-external constructor has no body."); } }*/ classTypeParametersAreInScope = false; currentMember = null; exitTreeNode(node); } @override void visitClass(Class node) { enterTreeNode(node); fileUri = checkLocation(node, node.name, node.fileUri); currentClass = node; declareTypeParameters(node.typeParameters); TreeNode? oldParent = enterParent(node); classTypeParametersAreInScope = false; visitList(node.annotations, this); classTypeParametersAreInScope = true; visitList(node.typeParameters, this); visitList(node.fields, this); visitList(node.constructors, this); visitList(node.procedures, this); exitParent(oldParent); undeclareTypeParameters(node.typeParameters); currentClass = null; exitTreeNode(node); } @override void visitFunctionNode(FunctionNode node) { enterTreeNode(node); declareTypeParameters(node.typeParameters); bool savedInCatchBlock = inCatchBlock; AsyncMarker savedAsyncMarker = currentAsyncMarker; currentAsyncMarker = node.asyncMarker; if (!isOutline) { if (node.asyncMarker == AsyncMarker.Async && node.emittedValueType == null) { problem(node, "No future value type set for async function in opt-in library."); } TreeNode? parent = node.parent; if (parent is! Procedure || !parent.isAbstract && !parent.isSynthetic && !parent.isSyntheticForwarder) { for (int positionalIndex = 0; positionalIndex < node.positionalParameters.length; positionalIndex++) { if (positionalIndex >= node.requiredParameterCount) { VariableDeclaration positionalParameter = node.positionalParameters[positionalIndex]; if (positionalParameter.initializer == null) { problem( positionalParameter, "An optional positional parameter is expected to have a " "default value initializer, defined or synthesized."); } } } for (VariableDeclaration namedParameter in node.namedParameters) { if (!namedParameter.isRequired && namedParameter.initializer == null) { problem( namedParameter, "An optional named parameter is expected to have a default " "value initializer, defined or synthesized."); } } } } inCatchBlock = false; visitWithLocalScope(node); inCatchBlock = savedInCatchBlock; currentAsyncMarker = savedAsyncMarker; undeclareTypeParameters(node.typeParameters); exitTreeNode(node); } @override void visitFunctionType(FunctionType node) { for (int i = 1; i < node.namedParameters.length; ++i) { if (node.namedParameters[i - 1].compareTo(node.namedParameters[i]) >= 0) { problem(currentParent, "Named parameters are not sorted on function type ($node)."); } } declareStructuralParameters(node.typeParameters); visitList(node.positionalParameters, this); visitList(node.namedParameters, this); node.returnType.accept(this); undeclareStructuralParameters(node.typeParameters); } @override void visitBlock(Block node) { visitWithLocalScope(node); } @override void visitForStatement(ForStatement node) { visitWithLocalScope(node); } @override void visitForInStatement(ForInStatement node) { visitWithLocalScope(node); } @override void visitLet(Let node) { if (_isCompileTimeErrorEncoding(node)) return; visitWithLocalScope(node); } @override void visitInvalidExpression(InvalidExpression node) { return; } @override void visitBlockExpression(BlockExpression node) { enterTreeNode(node); int stackHeight = enterLocalScope(); // Do not visit the block directly because the value expression needs to // be in its scope. TreeNode? oldParent = enterParent(node); enterParent(node.body); for (int i = 0; i < node.body.statements.length; ++i) { node.body.statements[i].accept(this); } exitParent(node); node.value.accept(this); exitParent(oldParent); exitLocalScope(stackHeight); exitTreeNode(node); } @override void visitCatch(Catch node) { bool savedInCatchBlock = inCatchBlock; inCatchBlock = true; visitWithLocalScope(node); inCatchBlock = savedInCatchBlock; } @override void visitReturnStatement(ReturnStatement node) { switch (currentAsyncMarker) { case AsyncMarker.Sync: case AsyncMarker.Async: // ok break; case AsyncMarker.SyncStar: case AsyncMarker.AsyncStar: if (node.expression != null) { problem( node, "Return statement in function with async marker: " "$currentAsyncMarker"); } break; } super.visitReturnStatement(node); } @override void visitYieldStatement(YieldStatement node) { switch (currentAsyncMarker) { case AsyncMarker.Sync: case AsyncMarker.Async: problem( node, "Yield statement in function with async marker: " "$currentAsyncMarker"); break; case AsyncMarker.SyncStar: case AsyncMarker.AsyncStar: // ok break; } super.visitYieldStatement(node); } @override void visitRethrow(Rethrow node) { if (!inCatchBlock) { problem(node, "Rethrow must be inside a Catch block."); } } @override void visitVariableDeclaration(VariableDeclaration node) { enterTreeNode(node); TreeNode? parent = node.parent; if (parent is! Block && !(parent is Catch && parent.body != node) && !(parent is FunctionNode && parent.body != node) && parent is! FunctionDeclaration && !(parent is ForStatement && parent.body != node) && !(parent is ForInStatement && parent.body != node) && parent is! Let && parent is! LocalInitializer && parent is! Typedef) { problem( node, "VariableDeclaration must be a direct child of a Block, " "not ${parent.runtimeType}."); } visitChildren(node); declareVariable(node); if (afterConst && node.isConst) { Expression? initializer = node.initializer; if (constantsAreAlwaysInlined) { if (!(initializer is InvalidExpression || initializer is ConstantExpression && initializer.constant is UnevaluatedConstant)) { problem(node, "Constant VariableDeclaration"); } } } exitTreeNode(node); } @override void visitVariableGet(VariableGet node) { enterTreeNode(node); checkVariableInScope(node.variable, node); visitChildren(node); if (constantsAreAlwaysInlined && afterConst && node.variable.isConst && !inUnevaluatedConstant) { problem(node, "VariableGet of const variable '${node.variable}'."); } exitTreeNode(node); } @override void visitVariableSet(VariableSet node) { enterTreeNode(node); checkVariableInScope(node.variable, node); visitChildren(node); exitTreeNode(node); } @override void visitStaticGet(StaticGet node) { enterTreeNode(node); visitChildren(node); // TODO(johnniwinther): Can this be deleted now? // Currently Constructor.hasGetter returns `false` even though the CFE uses // it as a getter for internal purposes: // // CFE is letting all call site of a redirecting constructor be resolved // to the real target. In order to resolve it, it seems to add a body into // the redirecting-factory constructor which caches the target constructor. // That cache is via a `StaticGet(real-constructor)` node, which we make // here pass the verifier. if (!node.target.hasGetter && node.target is! Constructor) { problem(node, "StaticGet of '${node.target}' without getter."); } if (node.target.isInstanceMember) { problem(node, "StaticGet of '${node.target}' that's an instance member."); } if (constantsAreAlwaysInlined && afterConst && node.target is Field && node.target.isConst) { problem(node, "StaticGet of const field '${node.target}'."); } exitTreeNode(node); } @override void visitStaticSet(StaticSet node) { enterTreeNode(node); visitChildren(node); if (!node.target.hasSetter) { problem(node, "StaticSet to '${node.target}' without setter."); } if (node.target.isInstanceMember) { problem(node, "StaticSet to '${node.target}' that's an instance member."); } exitTreeNode(node); } @override void visitStaticInvocation(StaticInvocation node) { enterTreeNode(node); checkTargetedInvocation(node.target, node); if (node.target.isInstanceMember) { problem(node, "StaticInvocation of '${node.target}' that's an instance member."); } if (node.isConst && !(node.target.isConst && node.target.isExternal && node.target.kind == ProcedureKind.Factory) && !(node.target.isConst && node.target.isExtensionTypeMember)) { problem( node, "Constant StaticInvocation of '${node.target}' that isn't" " a const external factory or a const extension type constructor."); } if (afterConst && node.isConst && !inUnevaluatedConstant) { problem(node, "Constant StaticInvocation."); } exitTreeNode(node); } @override void visitTypedefTearOff(TypedefTearOff node) { _checkTypedefTearOff(node); declareStructuralParameters(node.structuralParameters); super.visitTypedefTearOff(node); undeclareStructuralParameters(node.structuralParameters); } void checkTargetedInvocation(Member target, InvocationExpression node) { visitChildren(node); if (target.function == null) { problem(node, "${node.runtimeType} without function."); } if (!areArgumentsCompatible(node.arguments, target.function!)) { problem(node, "${node.runtimeType} with incompatible arguments for '${target}'."); } int expectedTypeParameters = target is Constructor ? target.enclosingClass.typeParameters.length : target.function!.typeParameters.length; if (node.arguments.types.length != expectedTypeParameters) { problem( node, "${node.runtimeType} with wrong number of type arguments" " for '${target}'."); } } @override void visitConstructorInvocation(ConstructorInvocation node) { enterTreeNode(node); checkTargetedInvocation(node.target, node); if (node.target.enclosingClass.isAbstract) { problem(node, "$node of abstract class ${node.target.enclosingClass}."); } if (node.isConst && !node.target.isConst) { problem( node, "Constant ConstructorInvocation fo '${node.target}' that" " isn't const."); } if (afterConst && node.isConst && !inUnevaluatedConstant) { problem(node, "Invocation of const constructor '${node.target}'."); } exitTreeNode(node); } bool areArgumentsCompatible(Arguments arguments, FunctionNode function) { if (arguments.positional.length < function.requiredParameterCount) { return false; } if (arguments.positional.length > function.positionalParameters.length) { return false; } namedLoop: for (int i = 0; i < arguments.named.length; ++i) { NamedExpression argument = arguments.named[i]; String name = argument.name; for (int j = 0; j < function.namedParameters.length; ++j) { if (function.namedParameters[j].name == name) continue namedLoop; } return false; } return true; } @override void visitListLiteral(ListLiteral node) { enterTreeNode(node); visitChildren(node); if (afterConst && node.isConst && !inUnevaluatedConstant) { problem(node, "Constant list literal."); } exitTreeNode(node); } @override void visitSetLiteral(SetLiteral node) { enterTreeNode(node); visitChildren(node); if (afterConst && node.isConst && !inUnevaluatedConstant) { problem(node, "Constant set literal."); } exitTreeNode(node); } @override void visitMapLiteral(MapLiteral node) { enterTreeNode(node); visitChildren(node); if (afterConst && node.isConst && !inUnevaluatedConstant) { problem(node, "Constant map literal."); } exitTreeNode(node); } @override void visitSymbolLiteral(SymbolLiteral node) { enterTreeNode(node); if (afterConst && !inUnevaluatedConstant) { problem(node, "Symbol literal."); } exitTreeNode(node); } @override void visitContinueSwitchStatement(ContinueSwitchStatement node) { enterTreeNode(node); if (node.target.parent == null) { problem(node, "Target has no parent."); } else { SwitchStatement statement = node.target.parent as SwitchStatement; for (SwitchCase switchCase in statement.cases) { if (switchCase == node.target) { exitTreeNode(node); return; } } problem(node, "Switch case isn't child of parent."); } exitTreeNode(node); } @override void visitInstanceConstant(InstanceConstant constant) { constant.visitChildren(this); if (constant.typeArguments.length != constant.classNode.typeParameters.length) { problem( currentParent, "Constant $constant provides ${constant.typeArguments.length}" " type arguments, but the class declares" " ${constant.classNode.typeParameters.length} parameters."); } Set superClasses = {}; int fieldCount = 0; for (Class? cls = constant.classNode; cls != null; cls = cls.superclass) { superClasses.add(cls); for (Field f in cls.fields) { if (!f.isStatic && !f.isConst) fieldCount++; } } if (constant.fieldValues.length != fieldCount) { problem( currentParent, "Constant $constant provides ${constant.fieldValues.length}" " field values, but the class declares" " $fieldCount fields."); } for (Reference fieldRef in constant.fieldValues.keys) { Field field = fieldRef.asField; if (!superClasses.contains(field.enclosingClass)) { problem( currentParent, "Constant $constant refers to field $field," " which does not belong to the right class."); } } } @override void visitUnevaluatedConstant(UnevaluatedConstant constant) { bool savedInUnevaluatedConstant = inUnevaluatedConstant; inUnevaluatedConstant = true; TreeNode? oldParent = currentParent; currentParent = null; constant.expression.accept(this); currentParent = oldParent; inUnevaluatedConstant = savedInUnevaluatedConstant; } @override void defaultMemberReference(Member node) { if (node.transformerFlags & TransformerFlag.seenByVerifier == 0) { problem( node, "Dangling reference to '$node', parent is: '${node.parent}'."); } } @override void visitClassReference(Class node) { if (!classes.contains(node)) { problem( node, "Dangling reference to '$node', parent is: '${node.parent}'."); } } @override void visitTypedefReference(Typedef node) { if (!typedefs.contains(node)) { problem( node, "Dangling reference to '$node', parent is: '${node.parent}'"); } } @override void visitTypeParameterType(TypeParameterType node) { TypeParameter parameter = node.parameter; GenericDeclaration? declaration = parameter.declaration; if (!typeParametersInScope.contains(parameter)) { problem( currentParent, "Type parameter '$parameter' referenced out of" " scope, declaration is: '${declaration}'."); } if (declaration is Class && !classTypeParametersAreInScope) { problem( currentParent, "Type parameter '$parameter' referenced from" " static context, declaration is: '${declaration}'."); } defaultDartType(node); } @override void visitInterfaceType(InterfaceType node) { if (isNullType(node) && node.nullability != Nullability.nullable) { problem(localContext, "Found a not nullable Null type: ${node}"); } defaultDartType(node); if (node.typeArguments.length != node.classNode.typeParameters.length) { problem( currentParent, "Type $node provides ${node.typeArguments.length}" " type arguments, but the class declares" " ${node.classNode.typeParameters.length} parameters."); } if (node.classNode.isAnonymousMixin) { bool isOk = false; if (currentParent is FunctionNode) { TreeNode? functionNodeParent = currentParent!.parent; if (functionNodeParent is Constructor) { if (functionNodeParent.parent == node.classNode) { // We only allow references to anonymous mixins in types as the // return type of its own constructor. isOk = true; } } } if (!isOk) { problem( currentParent, "Type $node references an anonymous mixin class."); } } } @override void visitTypedefType(TypedefType node) { checkTypedef(node.typedefNode); defaultDartType(node); if (node.typeArguments.length != node.typedefNode.typeParameters.length) { problem( currentParent, "The typedef type $node provides ${node.typeArguments.length}" " type arguments, but the typedef declares" " ${node.typedefNode.typeParameters.length} parameters."); } } @override void visitConstantExpression(ConstantExpression node) { enterTreeNode(node); inTreeNode(node, () { bool oldInConstant = inConstant; node.type.accept(this); // Only visit the [Constant] in constant context. inConstant = true; node.constant.accept(this); inConstant = oldInConstant; }); exitTreeNode(node); } @override void visitTypeParameter(TypeParameter node) { if (identical(node.bound, TypeParameter.unsetBoundSentinel)) { problem(node, "Unset bound on type parameter $node"); } if (identical(node.defaultType, TypeParameter.unsetDefaultTypeSentinel)) { problem(node, "Unset default type on type parameter $node"); } // ignore: deprecated_member_use_from_same_package if (node.parent == null) { // TODO(johnniwinther): Enable this check. // problem(node, "Type parameter without parent: $node"); node.visitChildren(this); } else { visitChildren(node); } } @override void visitTypedefTearOffConstant(TypedefTearOffConstant node) { _checkTypedefTearOff(node); declareStructuralParameters(node.parameters); super.visitTypedefTearOffConstant(node); undeclareStructuralParameters(node.parameters); } void _checkInterfaceTarget(Expression node, Member interfaceTarget) { if (!interfaceTarget.isInstanceMember) { problem( node, "Interface target $interfaceTarget is not an instance member."); } if (interfaceTarget is Procedure && interfaceTarget.stubKind == ProcedureStubKind.RepresentationField) { problem(node, "Representation field used as interface target: $interfaceTarget."); } if (interfaceTarget.enclosingClass == null) { problem( node, "Interface target $interfaceTarget does not have an " "enclosing class."); } } @override void visitInstanceInvocation(InstanceInvocation node) { if (node.name != node.interfaceTarget.name) { problem( node, "Instance invocation with name '${node.name}' has a " "target with name '${node.interfaceTarget.name}'."); } _checkInterfaceTarget(node, node.interfaceTarget); super.visitInstanceInvocation(node); } @override void visitInstanceGet(InstanceGet node) { if (node.name != node.interfaceTarget.name) { problem( node, "Instance get with name '${node.name}' has a " "target with name '${node.interfaceTarget.name}'."); } _checkInterfaceTarget(node, node.interfaceTarget); super.visitInstanceGet(node); } @override void visitInstanceTearOff(InstanceTearOff node) { if (node.name != node.interfaceTarget.name) { problem( node, "Instance tear-off with name '${node.name}' has a " "target with name '${node.interfaceTarget.name}'."); } _checkInterfaceTarget(node, node.interfaceTarget); super.visitInstanceTearOff(node); } @override void visitInstanceSet(InstanceSet node) { if (node.name != node.interfaceTarget.name) { problem( node, "Instance set with name '${node.name}' has a " "target with name '${node.interfaceTarget.name}'."); } _checkInterfaceTarget(node, node.interfaceTarget); super.visitInstanceSet(node); } /// Invoked by all visit methods if the visited node is a [TreeNode]. // TODO(johnniwinther): Merge this with enter/exitParent. void enterTreeNode(TreeNode node) { treeNodeStack.add(node); testLocation(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(_getLocation(node), node)) continue; return node; } return null; } TreeNode? getSameLibraryLastSeenTreeNode({bool withLocation = false}) { if (treeNodeStack.isEmpty) return null; if (currentLibrary == null) return null; for (int i = treeNodeStack.length - 1; i >= 0; --i) { TreeNode node = treeNodeStack[i]; Location? location = _getLocation(node); if (withLocation && !_hasLocation(location, node)) continue; if (location != null && location.file == currentLibrary!.fileUri) { return node; } } return null; } /// Returns the `TreeNode.location` while handling [RangeError]s caused by /// file offsets not within the range of the enclosing file. Location? _getLocation(TreeNode node, {bool allowInvalidLocation = false}) { try { return node.location; } on RangeError catch (e) { if (allowInvalidLocation || target.verification.allowInvalidLocation(stage, node)) { return null; } problem( node, "Invalid location with target '${target.name}' on " "${node} (${node.runtimeType}): $e"); } return null; } bool _hasLocation(Location? location, TreeNode node) { return location != null && node.fileOffset != TreeNode.noOffset; } bool _isInSameLibrary(Library? library, TreeNode node) { if (library == null) return false; Location? location = _getLocation(node); if (location == null) return false; return library.fileUri == location.file; } TreeNode? get localContext { TreeNode? result = getSameLibraryLastSeenTreeNode(withLocation: true); if (result == null && currentClassOrExtensionOrMember != null && _isInSameLibrary(currentLibrary, currentClassOrExtensionOrMember!)) { result = currentClassOrExtensionOrMember; } return result; } TreeNode? get remoteContext { TreeNode? result = getLastSeenTreeNode(withLocation: true); if (result != null && _isInSameLibrary(currentLibrary, result)) { result = null; } return result; } // We disable the location test for now, at least these tests currently fail: // outline/dartdevc/factory_patch/main // outline/general/constructor_patch/main // outline/general/factory_patch/main // outline/general/mixin_from_patch/main // outline/general/multiple_class_patches/main // outline/general/patch_extends_implements/main // outline/nnbd/platform_optional_parameters/main // pkg/front_end/test/macros/application/macro_application_test.dart -p \ // subtypes.dart static const bool doTestLocation = false; void testLocation(TreeNode node) { if (!doTestLocation) return; // When these comes from patching (and in the future from augmentation) they // don't point correctly. if (node is LibraryDependency || node is LibraryPart) return; try { if (node.fileOffset != TreeNode.noOffset) { node.location; } } catch (e) { problem( node, "${node.runtimeType} crashes when asked for location: '$e'", context: node); } } 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 (node.fileOffset == TreeNode.noOffset && !target.verification.allowNoFileOffset(stage, node)) { 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.containsSuperCalls) { 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 void visitAsExpression(AsExpression node) { enterTreeNode(node); super.visitAsExpression(node); if (node.fileOffset == TreeNode.noOffset && !node.isUnchecked && !target.verification.allowNoFileOffset(stage, node)) { TreeNode? parent = node.parent; while (parent != null) { if (parent.fileOffset != TreeNode.noOffset) 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. enterTreeNode(node); super.visitExpressionStatement(node); exitTreeNode(node); } bool isNullType(DartType node) => node is NullType; bool isObjectClass(Class c) { return c.name == "Object" && c.enclosingLibrary.importUri.isScheme("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) { if (!inConstant && node.nullability == Nullability.legacy) { problem(localContext, "Unexpected appearance of the legacy type $node outside a constant.", origin: remoteContext); } if (!AllowedTypes.isAllowed(node, inConstant: inConstant)) { final TreeNode? localContext = this.localContext; final TreeNode? remoteContext = this.remoteContext; problem( localContext, "Unexpected appearance of the disallowed type $node" "${inConstant ? " inside a constant" : ""}.", origin: remoteContext); } super.defaultDartType(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); } void _checkConstructorTearOff(Node node, Member tearOffTarget) { if (tearOffTarget.enclosingLibrary.importUri.isScheme('dart')) { // Platform libraries are not compilation with test flags and might // contain tear-offs not expected when testing lowerings. return; } if (tearOffTarget is Constructor && target.isConstructorTearOffLoweringEnabled) { problem( node is TreeNode ? node : getLastSeenTreeNode(), '${node.runtimeType} nodes for generative constructors should be ' 'lowered for target "${target.name}".'); } if (tearOffTarget is Procedure && tearOffTarget.isFactory && target.isFactoryTearOffLoweringEnabled) { problem( node is TreeNode ? node : getLastSeenTreeNode(), '${node.runtimeType} nodes for factory constructors should be ' 'lowered for target "${target.name}".'); } } @override void visitConstructorTearOff(ConstructorTearOff node) { _checkConstructorTearOff(node, node.target); super.visitConstructorTearOff(node); } @override void visitConstructorTearOffConstant(ConstructorTearOffConstant node) { _checkConstructorTearOff(node, node.target); super.visitConstructorTearOffConstant(node); } void _checkTypedefTearOff(Node node) { if (target.isTypedefTearOffLoweringEnabled) { problem( node is TreeNode ? node : getLastSeenTreeNode(), '${node.runtimeType} nodes for typedefs should be ' 'lowered for target "${target.name}".'); } } void _checkRedirectingFactoryTearOff(Node node) { if (target.isRedirectingFactoryTearOffLoweringEnabled) { problem( node is TreeNode ? node : getLastSeenTreeNode(), 'ConstructorTearOff nodes for redirecting factories should be ' 'lowered for target "${target.name}".'); } } @override void visitRedirectingFactoryTearOff(RedirectingFactoryTearOff node) { _checkRedirectingFactoryTearOff(node); super.visitRedirectingFactoryTearOff(node); } @override void visitRedirectingFactoryTearOffConstant( RedirectingFactoryTearOffConstant node) { _checkRedirectingFactoryTearOff(node); super.visitRedirectingFactoryTearOffConstant(node); } @override void visitSwitchStatement(SwitchStatement node) { if (node.expressionTypeInternal == null) { problem(node, 'SwitchStatement.expressionType has not been set.'); } super.visitSwitchStatement(node); } } class VerifyGetStaticType extends RecursiveVisitor { final TypeEnvironment env; Member? currentMember; final StatefulStaticTypeContext _staticTypeContext; VerifyGetStaticType(this.env) : _staticTypeContext = new StatefulStaticTypeContext.stacked(env); @override void visitLibrary(Library node) { _staticTypeContext.enterLibrary(node); super.visitLibrary(node); _staticTypeContext.leaveLibrary(node); } @override void visitField(Field node) { currentMember = node; _staticTypeContext.enterMember(node); super.visitField(node); _staticTypeContext.leaveMember(node); currentMember = node; } @override void visitProcedure(Procedure node) { currentMember = node; _staticTypeContext.enterMember(node); super.visitProcedure(node); _staticTypeContext.leaveMember(node); currentMember = node; } @override void visitConstructor(Constructor node) { currentMember = node; _staticTypeContext.enterMember(node); super.visitConstructor(node); _staticTypeContext.leaveMember(node); currentMember = null; } @override void visitLet(Let node) { if (_isCompileTimeErrorEncoding(node)) return; super.visitLet(node); } @override void visitInvalidExpression(InvalidExpression node) { return; } @override void defaultExpression(Expression node) { try { node.getStaticType(_staticTypeContext); } catch (_) { print('Error in $currentMember in ${currentMember?.fileUri}: ' '$node (${node.runtimeType})'); rethrow; } super.defaultExpression(node); } } void checkInitializers(Constructor constructor) { // TODO(ahe): I'll add more here in other CLs. } bool _isCompileTimeErrorEncoding(TreeNode? node) { return node is Let && node.variable.initializer is InvalidExpression; } class AllowedTypes implements DartTypeVisitor { static bool isAllowed(DartType type, {required bool inConstant}) { return type.accept(inConstant ? const AllowedTypes(inConstant: true) : const AllowedTypes(inConstant: false)); } final bool inConstant; const AllowedTypes({required this.inConstant}); @override bool visitAuxiliaryType(AuxiliaryType node) => false; @override bool visitDynamicType(DynamicType node) => true; @override bool visitFunctionType(FunctionType node) => true; @override bool visitFutureOrType(FutureOrType node) => true; @override bool visitExtensionType(ExtensionType node) => !inConstant; @override bool visitInterfaceType(InterfaceType node) => true; @override bool visitIntersectionType(IntersectionType node) => true; @override bool visitInvalidType(InvalidType node) => true; @override bool visitNeverType(NeverType node) => true; @override bool visitNullType(NullType node) => true; @override bool visitRecordType(RecordType node) => true; @override bool visitTypeParameterType(TypeParameterType node) => true; @override bool visitStructuralParameterType(StructuralParameterType node) => true; @override bool visitTypedefType(TypedefType node) => true; @override bool visitVoidType(VoidType node) => true; }