// 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 'transformations/flags.dart'; void verifyComponent(Component component) { VerifyingVisitor.check(component); } class VerificationError { final TreeNode context; final TreeNode node; final String details; VerificationError(this.context, this.node, this.details); 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 RecursiveVisitor { final Set classes = new Set(); final Set typedefs = new Set(); Set typeParametersInScope = new Set(); final List variableStack = []; final Map typedefState = {}; bool classTypeParametersAreInScope = false; /// If true, relax certain checks for *outline* mode. For example, don't /// attempt to validate constructor initializers. bool isOutline = false; bool inCatchBlock = false; Library currentLibrary; Member currentMember; Class currentClass; TreeNode currentParent; TreeNode get context => currentMember ?? currentClass; static void check(Component component) { component.accept(new VerifyingVisitor()); } defaultTreeNode(TreeNode node) { visitChildren(node); } problem(TreeNode node, String details, {TreeNode context}) { context ??= this.context; throw new VerificationError(context, node, details); } TreeNode enterParent(TreeNode node) { if (!identical(node.parent, currentParent)) { problem( node, "Incorrect parent pointer on ${node.runtimeType}:" " expected '${currentParent.runtimeType}'," " but found: '${node.parent.runtimeType}'.", context: currentParent); } var 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; } void visitChildren(TreeNode node) { var oldParent = enterParent(node); node.visitChildren(this); exitParent(oldParent); } void visitWithLocalScope(TreeNode node) { int stackHeight = enterLocalScope(); visitChildren(node); exitLocalScope(stackHeight); } 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; } void declareVariable(VariableDeclaration variable) { if (variable.flags & VariableDeclaration.FlagInScope != 0) { problem(variable, "Variable '$variable' declared more than once."); } variable.flags |= VariableDeclaration.FlagInScope; variableStack.add(variable); } void undeclareVariable(VariableDeclaration variable) { variable.flags &= ~VariableDeclaration.FlagInScope; } void declareTypeParameters(List parameters) { for (int i = 0; i < parameters.length; ++i) { var parameter = parameters[i]; if (parameter.bound == null) { problem( currentParent, "Missing bound for type parameter '$parameter'."); } if (!typeParametersInScope.add(parameter)) { problem(parameter, "Type parameter '$parameter' redeclared."); } } } void undeclareTypeParameters(List parameters) { typeParametersInScope.removeAll(parameters); } void checkVariableInScope(VariableDeclaration variable, TreeNode where) { if (variable.flags & VariableDeclaration.FlagInScope == 0) { problem(where, "Variable '$variable' used out of scope."); } } visitComponent(Component component) { try { for (var library in component.libraries) { for (var class_ in library.classes) { if (!classes.add(class_)) { problem(class_, "Class '$class_' declared more than once."); } } for (var typedef_ in library.typedefs) { if (!typedefs.add(typedef_)) { problem(typedef_, "Typedef '$typedef_' declared more than once."); } } library.members.forEach(declareMember); for (var class_ in library.classes) { class_.members.forEach(declareMember); } } visitChildren(component); } finally { for (var library in component.libraries) { library.members.forEach(undeclareMember); for (var class_ in library.classes) { class_.members.forEach(undeclareMember); } } variableStack.forEach(undeclareVariable); } } void visitLibrary(Library node) { currentLibrary = node; super.visitLibrary(node); currentLibrary = null; } void checkTypedef(Typedef node) { var 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); typedefState[node] = TypedefState.BeingChecked; var savedTypeParameters = typeParametersInScope; typeParametersInScope = node.typeParameters.toSet(); var 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; } visitTypedef(Typedef node) { checkTypedef(node); // Enter and exit the node to check the parent pointer on the typedef node. exitParent(enterParent(node)); } visitField(Field node) { currentMember = node; var oldParent = enterParent(node); bool isTopLevel = node.parent == currentLibrary; if (isTopLevel && !node.isStatic) { problem(node, "The top-level field '${node.name.name}' should be static", context: node); } if (node.isConst && !node.isStatic) { problem(node, "The const field '${node.name.name}' should be static", context: node); } classTypeParametersAreInScope = !node.isStatic; node.initializer?.accept(this); node.type.accept(this); classTypeParametersAreInScope = false; visitList(node.annotations, this); exitParent(oldParent); currentMember = null; } visitProcedure(Procedure node) { currentMember = node; var oldParent = enterParent(node); classTypeParametersAreInScope = !node.isStatic; node.function.accept(this); classTypeParametersAreInScope = false; visitList(node.annotations, this); exitParent(oldParent); currentMember = null; } visitConstructor(Constructor node) { currentMember = node; classTypeParametersAreInScope = true; // The constructor member needs special treatment due to parameters being // in scope in the initializer list. var 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); classTypeParametersAreInScope = false; currentMember = null; } visitClass(Class node) { currentClass = node; declareTypeParameters(node.typeParameters); var 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; } visitFunctionNode(FunctionNode node) { declareTypeParameters(node.typeParameters); bool savedInCatchBlock = inCatchBlock; inCatchBlock = false; visitWithLocalScope(node); inCatchBlock = savedInCatchBlock; undeclareTypeParameters(node.typeParameters); } 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)."); } } declareTypeParameters(node.typeParameters); for (var typeParameter in node.typeParameters) { typeParameter.bound?.accept(this); } visitList(node.positionalParameters, this); visitList(node.namedParameters, this); node.returnType.accept(this); undeclareTypeParameters(node.typeParameters); } visitBlock(Block node) { visitWithLocalScope(node); } visitForStatement(ForStatement node) { visitWithLocalScope(node); } visitForInStatement(ForInStatement node) { visitWithLocalScope(node); } visitLet(Let node) { visitWithLocalScope(node); } visitBlockExpression(BlockExpression 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); } visitCatch(Catch node) { bool savedInCatchBlock = inCatchBlock; inCatchBlock = true; visitWithLocalScope(node); inCatchBlock = savedInCatchBlock; } @override visitRethrow(Rethrow node) { if (!inCatchBlock) { problem(node, "Rethrow must be inside a Catch block."); } } visitVariableDeclaration(VariableDeclaration node) { var 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) { problem( node, "VariableDeclaration must be a direct child of a Block, " "not ${parent.runtimeType}."); } visitChildren(node); declareVariable(node); } visitVariableGet(VariableGet node) { checkVariableInScope(node.variable, node); visitChildren(node); } visitVariableSet(VariableSet node) { checkVariableInScope(node.variable, node); visitChildren(node); } @override visitStaticGet(StaticGet node) { visitChildren(node); if (node.target == null) { problem(node, "StaticGet without target."); } // Currently Constructor.hasGetter returns `false` even though fasta uses it // as a getter for internal purposes: // // Fasta 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."); } } @override visitStaticSet(StaticSet node) { visitChildren(node); if (node.target == null) { problem(node, "StaticSet without target."); } 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."); } } @override visitStaticInvocation(StaticInvocation 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)) { problem( node, "Constant StaticInvocation of '${node.target}' that isn't" " a const external factory."); } } void checkTargetedInvocation(Member target, InvocationExpression node) { visitChildren(node); if (target == null) { problem(node, "${node.runtimeType} without target."); } 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 visitDirectPropertyGet(DirectPropertyGet node) { visitChildren(node); if (node.target == null) { problem(node, "DirectPropertyGet without target."); } if (!node.target.hasGetter) { problem(node, "DirectPropertyGet of '${node.target}' without getter."); } if (!node.target.isInstanceMember) { problem( node, "DirectPropertyGet of '${node.target}' that isn't an" " instance member."); } } @override visitDirectPropertySet(DirectPropertySet node) { visitChildren(node); if (node.target == null) { problem(node, "DirectPropertySet without target."); } if (!node.target.hasSetter) { problem(node, "DirectPropertySet of '${node.target}' without setter."); } if (!node.target.isInstanceMember) { problem(node, "DirectPropertySet of '${node.target}' that is static."); } } @override visitDirectMethodInvocation(DirectMethodInvocation node) { checkTargetedInvocation(node.target, node); if (node.receiver == null) { problem(node, "DirectMethodInvocation without receiver."); } } @override visitConstructorInvocation(ConstructorInvocation node) { checkTargetedInvocation(node.target, node); if (node.target.enclosingClass.isAbstract) { problem(node, "ConstructorInvocation of abstract class."); } if (node.isConst && !node.target.isConst) { problem( node, "Constant ConstructorInvocation fo '${node.target}' that" " isn't const."); } } 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) { var 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 visitContinueSwitchStatement(ContinueSwitchStatement node) { if (node.target == null) { problem(node, "No target."); } else if (node.target.parent == null) { problem(node, "Target has no parent."); } else { SwitchStatement statement = node.target.parent; for (SwitchCase switchCase in statement.cases) { if (switchCase == node.target) return; } problem(node, "Switch case isn't child of parent."); } } @override defaultMemberReference(Member node) { if (node.transformerFlags & TransformerFlag.seenByVerifier == 0) { problem( node, "Dangling reference to '$node', parent is: '${node.parent}'."); } } @override visitClassReference(Class node) { if (!classes.contains(node)) { problem( node, "Dangling reference to '$node', parent is: '${node.parent}'."); } } @override visitTypedefReference(Typedef node) { if (!typedefs.contains(node)) { problem( node, "Dangling reference to '$node', parent is: '${node.parent}'"); } } @override visitTypeParameterType(TypeParameterType node) { var parameter = node.parameter; if (!typeParametersInScope.contains(parameter)) { problem( currentParent, "Type parameter '$parameter' referenced out of" " scope, parent is: '${parameter.parent}'."); } if (parameter.parent is Class && !classTypeParametersAreInScope) { problem( currentParent, "Type parameter '$parameter' referenced from" " static context, parent is: '${parameter.parent}'."); } } @override visitInterfaceType(InterfaceType node) { node.visitChildren(this); 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."); } } @override visitTypedefType(TypedefType node) { checkTypedef(node.typedefNode); node.visitChildren(this); 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."); } } } class CheckParentPointers extends Visitor { static void check(TreeNode node) { node.accept(new CheckParentPointers(node.parent)); } TreeNode parent; CheckParentPointers([this.parent]); defaultTreeNode(TreeNode node) { if (node.parent != parent) { throw new VerificationError( parent, node, "Parent pointer on '${node.runtimeType}' " "is '${node.parent.runtimeType}' " "but should be '${parent.runtimeType}'."); } var oldParent = parent; parent = node; node.visitChildren(this); parent = oldParent; } } void checkInitializers(Constructor constructor) { // TODO(ahe): I'll add more here in other CLs. }