def validate_modification(self, node: Union[vy_ast.Assign, vy_ast.AugAssign]) -> None: """ Validate an attempt to modify this value. Raises if the value is a constant or involves an invalid operation. Arguments --------- node : Assign | AugAssign Vyper ast node of the modifying action. """ if self.location == DataLocation.CALLDATA: raise ImmutableViolation("Cannot write to calldata", node) if self.is_constant: raise ImmutableViolation("Constant value cannot be written to", node) if self.is_immutable: if node.get_ancestor(vy_ast.FunctionDef).get("name") != "__init__": raise ImmutableViolation("Immutable value cannot be written to", node) if self._modification_count: raise ImmutableViolation( "Immutable value cannot be modified after assignment", node ) self._modification_count += 1 if isinstance(node, vy_ast.AugAssign): self.validate_numeric_op(node)
def validate_modification(self, node: Union[vy_ast.Assign, vy_ast.AugAssign]) -> None: """ Validate an attempt to modify this value. Raises if the value is a constant or involves an invalid operation. Arguments --------- node : Assign | AugAssign Vyper ast node of the modifying action. """ if self.location == DataLocation.CALLDATA: raise ImmutableViolation("Cannot write to calldata", node) if self.is_immutable: raise ImmutableViolation("Immutable value cannot be written to", node) if isinstance(node, vy_ast.AugAssign): self.validate_numeric_op(node)
def validate_modification( self, node: Union[vy_ast.Assign, vy_ast.AugAssign, vy_ast.Call], mutability: Any, # should be StateMutability, import cycle ) -> None: """ Validate an attempt to modify this value. Raises if the value is a constant or involves an invalid operation. Arguments --------- node : Assign | AugAssign | Call Vyper ast node of the modifying action. mutability: StateMutability The mutability of the context (e.g., pure function) we are currently in """ # TODO: break this cycle, probably by moving this to validation module from vyper.semantics.types.function import StateMutability if mutability <= StateMutability.VIEW and self.location == DataLocation.STORAGE: raise StateAccessViolation( f"Cannot modify storage in a {mutability.value} function", node) if self.location == DataLocation.CALLDATA: raise ImmutableViolation("Cannot write to calldata", node) if self.is_constant: raise ImmutableViolation("Constant value cannot be written to", node) if self.is_immutable: if node.get_ancestor(vy_ast.FunctionDef).get("name") != "__init__": raise ImmutableViolation( "Immutable value cannot be written to", node) if self._modification_count: raise ImmutableViolation( "Immutable value cannot be modified after assignment", node) self._modification_count += 1 if isinstance(node, vy_ast.AugAssign): self.validate_numeric_op(node)
def visit_For(self, node): if isinstance(node.iter, vy_ast.Subscript): raise StructureException("Cannot iterate over a nested list", node.iter) if isinstance(node.iter, vy_ast.Call): # iteration via range() if node.iter.get("func.id") != "range": raise IteratorException( "Cannot iterate over the result of a function call", node.iter ) validate_call_args(node.iter, (1, 2)) args = node.iter.args if len(args) == 1: # range(CONSTANT) if not isinstance(args[0], vy_ast.Num): raise StateAccessViolation("Value must be a literal", node) if args[0].value <= 0: raise StructureException("For loop must have at least 1 iteration", args[0]) validate_expected_type(args[0], Uint256Definition()) type_list = get_possible_types_from_node(args[0]) else: validate_expected_type(args[0], IntegerAbstractType()) type_list = get_common_types(*args) if not isinstance(args[0], vy_ast.Constant): # range(x, x + CONSTANT) if not isinstance(args[1], vy_ast.BinOp) or not isinstance( args[1].op, vy_ast.Add ): raise StructureException( "Second element must be the first element plus a literal value", args[0], ) if not vy_ast.compare_nodes(args[0], args[1].left): raise StructureException( "First and second variable must be the same", args[1].left ) if not isinstance(args[1].right, vy_ast.Int): raise InvalidLiteral("Literal must be an integer", args[1].right) if args[1].right.value < 1: raise StructureException( f"For loop has invalid number of iterations ({args[1].right.value})," " the value must be greater than zero", args[1].right, ) else: # range(CONSTANT, CONSTANT) if not isinstance(args[1], vy_ast.Int): raise InvalidType("Value must be a literal integer", args[1]) validate_expected_type(args[1], IntegerAbstractType()) if args[0].value >= args[1].value: raise StructureException("Second value must be > first value", args[1]) else: # iteration over a variable or literal list type_list = [ i.value_type for i in get_possible_types_from_node(node.iter) if isinstance(i, ArrayDefinition) ] if not type_list: raise InvalidType("Not an iterable type", node.iter) if next((i for i in type_list if isinstance(i, ArrayDefinition)), False): raise StructureException("Cannot iterate over a nested list", node.iter) if isinstance(node.iter, (vy_ast.Name, vy_ast.Attribute)): # check for references to the iterated value within the body of the loop assign = _check_iterator_assign(node.iter, node) if assign: raise ImmutableViolation("Cannot modify array during iteration", assign) if node.iter.get("value.id") == "self": # check if iterated value may be modified by function calls inside the loop iter_name = node.iter.attr for call_node in node.get_descendants(vy_ast.Call, {"func.value.id": "self"}): fn_name = call_node.func.attr fn_node = self.vyper_module.get_children(vy_ast.FunctionDef, {"name": fn_name})[0] if _check_iterator_assign(node.iter, fn_node): # check for direct modification raise ImmutableViolation( f"Cannot call '{fn_name}' inside for loop, it potentially " f"modifies iterated storage variable '{iter_name}'", call_node, ) for name in self.namespace["self"].members[fn_name].recursive_calls: # check for indirect modification fn_node = self.vyper_module.get_children(vy_ast.FunctionDef, {"name": name})[0] if _check_iterator_assign(node.iter, fn_node): raise ImmutableViolation( f"Cannot call '{fn_name}' inside for loop, it may call to '{name}' " f"which potentially modifies iterated storage variable '{iter_name}'", call_node, ) for_loop_exceptions = [] iter_name = node.target.id for type_ in type_list: # type check the for loop body using each possible type for iterator value type_ = copy.deepcopy(type_) type_.is_immutable = True with self.namespace.enter_scope(): try: self.namespace[iter_name] = type_ except VyperException as exc: raise exc.with_annotation(node) from None try: for n in node.body: self.visit(n) return except TypeMismatch as exc: for_loop_exceptions.append(exc) if len(set(str(i) for i in for_loop_exceptions)) == 1: # if every attempt at type checking raised the same exception raise for_loop_exceptions[0] # return an aggregate TypeMismatch that shows all possible exceptions # depending on which type is used types_str = [str(i) for i in type_list] given_str = f"{', '.join(types_str[:1])} or {types_str[-1]}" raise TypeMismatch( f"Iterator value '{iter_name}' may be cast as {given_str}, " "but type checking fails with all possible types:", node, *( (f"Casting '{iter_name}' as {type_}: {exc.message}", exc.annotations[0]) for type_, exc in zip(type_list, for_loop_exceptions) ), )