def create_new_event(self, create_call: ast.Call) -> Event: event = Event('') event_args = create_call.args if len(event_args) < 0: self._log_error( CompilerError.UnfilledArgument(line=create_call.lineno, col=create_call.col_offset, param=list( Builtin.NewEvent.args)[0])) elif len(event_args) > 0: args_type = self.get_type(event_args[0]) if not Type.list.is_type_of(args_type): self._log_error( CompilerError.MismatchedTypes( line=event_args[0].lineno, col=event_args[0].col_offset, expected_type_id=Type.list.identifier, actual_type_id=args_type.identifier)) else: for value in event_args[0].elts: if not isinstance(value, ast.Tuple): CompilerError.MismatchedTypes( line=value.lineno, col=value.col_offset, expected_type_id=Type.tuple.identifier, actual_type_id=self.get_type(value).identifier) elif len(value.elts) < 2: self._log_error( CompilerError.UnfilledArgument( line=value.lineno, col=value.col_offset, param=list(Builtin.NewEvent.args)[0])) elif not (isinstance(value.elts[0], ast.Str) and (isinstance(value.elts[1], ast.Name) and isinstance( self.get_symbol(value.elts[1].id), IType))): CompilerError.MismatchedTypes( line=value.lineno, col=value.col_offset, expected_type_id=Type.tuple.identifier, actual_type_id=self.get_type(value).identifier) else: arg_name = value.elts[0].s arg_type = self.get_symbol(value.elts[1].id) event.args[arg_name] = Variable(arg_type) if len(event_args) > 1: if not isinstance(event_args[1], ast.Str): name_type = self.get_type(event_args[1]) CompilerError.MismatchedTypes( line=event_args[1].lineno, col=event_args[1].col_offset, expected_type_id=Type.str.identifier, actual_type_id=name_type.identifier) else: event.name = event_args[1].s return event
def visit_Num(self, num: ast.Num) -> int: """ Verifies if the number is an integer :param num: the python ast number node :return: returns the value of the number """ if not isinstance(num.n, int): # only integer numbers are allowed self._log_error( CompilerError.InvalidType(num.lineno, num.col_offset, symbol_id=type(num.n).__name__) ) return num.n
def visit_arg(self, arg: ast.arg): """ Verifies if the argument of a function has a type annotation :param arg: the python ast arg node """ if arg.annotation is None: self._log_error( CompilerError.TypeHintMissing(arg.lineno, arg.col_offset, symbol_id=arg.arg) ) # continue to walk through the tree self.generic_visit(arg)
def visit_Constant(self, constant: ast.Constant) -> Any: """ Visitor of constant values node :param constant: the python ast constant value node :return: the value of the constant """ if isinstance(constant, ast.Num) and not isinstance(constant.value, int): # only integer numbers are allowed self._log_error( CompilerError.InvalidType(constant.lineno, constant.col_offset, symbol_id=type(constant.value).__name__) ) return constant.value
def visit_Module(self, module: ast.Module): """ Visitor of the module node Fills module symbol table :param module: """ mod: Module = Module() self._current_module = mod self._scope_stack.append(SymbolScope()) global_stmts = [] function_stmts = [] for stmt in module.body: if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)): function_stmts.append(stmt) elif not (isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant)): # don't evaluate constant expression - for example: string for documentation if self.visit(stmt) is not Builtin.Event: global_stmts.append(stmt) module.body = global_stmts + function_stmts for var_id, var in mod.variables.items(): # all static fields must be initialized if not mod.is_variable_assigned(var_id): self._log_error( CompilerError.UnresolvedReference( line=var.origin.lineno if var.origin is not None else 0, col=var.origin.col_offset if var.origin is not None else 0, symbol_id=var_id ) ) for stmt in function_stmts: result = self.visit(stmt) # don't evaluate the metadata function in the following analysers if result is Builtin.Metadata: module.body.remove(stmt) # TODO: include the body of the builtin methods to the ast # TODO: get module name self.modules['main'] = mod module_scope = self._scope_stack.pop() for symbol_id, symbol in module_scope.symbols.items(): if symbol_id in self._global_assigned_variables: mod.include_symbol(symbol_id, symbol) mod.assign_variable(symbol_id) self._global_assigned_variables.clear() self._current_module = None
def visit_UnaryOp(self, un_op: ast.UnaryOp) -> Optional[IType]: """ Verifies if the type of the operand is valid to the operation If the operation is valid, changes de Python operator by the Boa operator in the syntax tree :param un_op: the python ast unary operation node :return: the type of the result of the operation if the operation is valid. Otherwise, returns None :rtype: IType or None """ operator: Operator = self.get_operator(un_op.op) operand = self.visit(un_op.operand) if not isinstance(operator, Operator): # the operator is invalid or it was not implemented yet self._log_error( CompilerError.UnresolvedReference(un_op.lineno, un_op.col_offset, type(un_op.op).__name__) ) try: operation: UnaryOperation = self.get_un_op(operator, operand) if operation is None: self._log_error( CompilerError.NotSupportedOperation(un_op.lineno, un_op.col_offset, operator) ) elif not operation.is_supported: self._log_error( CompilerError.NotSupportedOperation(un_op.lineno, un_op.col_offset, operator) ) else: un_op.op = operation return operation.result except CompilerError.MismatchedTypes as raised_error: raised_error.line = un_op.lineno raised_error.col = un_op.col_offset # raises the exception with the line/col info self._log_error(raised_error)
def visit_Return(self, ret: ast.Return): """ Verifies if the return of the function is the same type as the return type annotation :param ret: the python ast return node """ ret_value: Any = self.visit(ret.value) if ret.value is not None else None if ret.value is not None and self.get_type(ret.value) is not Type.none: # multiple returns are not allowed if isinstance(ret.value, ast.Tuple): self._log_error( CompilerError.TooManyReturns(ret.lineno, ret.col_offset) ) return # it is returning something, but there is no type hint for return elif self._current_method.return_type is Type.none: self._log_error( CompilerError.TypeHintMissing(ret.lineno, ret.col_offset, symbol_id=self._current_method_id) ) return self.validate_type_variable_assign(ret, ret_value, self._current_method) # continue to walk through the tree self.generic_visit(ret)
def visit_Return(self, ret: ast.Return): """ Visitor of the function return node If the return is a name, verifies if the symbol is defined :param ret: the python ast return node """ if isinstance(ret.value, ast.Name): symbol_id = self.visit(ret.value) symbol = self.get_symbol(symbol_id) if symbol is None: # the symbol doesn't exists self._log_error( CompilerError.UnresolvedReference(ret.value.lineno, ret.value.col_offset, symbol_id) )
def validate_if(self, if_node: ast.AST): """ Verifies if the type of if test is valid :param if_node: the python ast if statement node :type if_node: ast.If or ast.IfExp """ test = self.visit(if_node.test) test_type: IType = self.get_type(test) if test_type is not Type.bool: self._log_error( CompilerError.MismatchedTypes( if_node.lineno, if_node.col_offset, actual_type_id=test_type.identifier, expected_type_id=Type.bool.identifier) )
def visit_Assign(self, assign: ast.Assign): """ Verifies if it is a multiple assignments statement :param assign: the python ast variable assignment node """ # multiple assignments if isinstance(assign.targets[0], ast.Tuple): self._log_error( CompilerError.NotSupportedOperation(assign.lineno, assign.col_offset, 'Multiple variable assignments') ) else: for target in assign.targets: self.validate_type_variable_assign(target, assign.value) # continue to walk through the tree self.generic_visit(assign)
def visit_Raise(self, raise_node: ast.Raise): """ Visitor of the raise node Verifies if the raised object is a exception :param raise_node: the python ast raise node """ raised = self.visit(raise_node.exc) raised_type: IType = self.get_type(raised) if raised_type is not Type.exception: self._log_error( CompilerError.MismatchedTypes( raise_node.lineno, raise_node.col_offset, actual_type_id=raised_type.identifier, expected_type_id=Type.exception.identifier ))
def _validate_IsInstanceMethod(self, method: IsInstanceMethod, args_types: List[IType]): """ Validates the arguments for `isinstance` method :param method: instance of the builtin method :param args_types: types of the arguments """ last_arg = self.call.args[-1] if isinstance(last_arg, ast.Tuple) and all( isinstance(tpe, (ast.Attribute, ast.Name)) for tpe in last_arg.elts): if len(last_arg.elts) == 1: # if the types tuple has only one type, remove it from inside the tuple last_arg = self.call.args[-1] = last_arg.elts[-1] args_types[-1] = args_types[-1].value_type elif len(last_arg.elts) > 1: # if there are more than one type, updates information in the instance of the method types: List[IType] = [ self.get_symbol_from_node(name) for name in last_arg.elts ] method.set_instance_type(types) self.call.args[-1] = last_arg.elts[-1] return from boa3.model.type.annotation.metatype import MetaType from boa3.model.type.type import Type is_ast_valid = (isinstance(last_arg, ast.Name) or (isinstance(last_arg, ast.NameConstant) and args_types[-1] is Type.none)) is_id_valid = (hasattr(last_arg, 'id') and last_arg.id != args_types[-1].identifier and last_arg.id != args_types[-1].raw_identifier and isinstance( self.get_type(last_arg, use_metadata=True), MetaType)) if not is_ast_valid or is_id_valid: # if the value is not the identifier of a type self._log_error( CompilerError.MismatchedTypes( last_arg.lineno, last_arg.col_offset, expected_type_id=type.__name__, actual_type_id=args_types[-1].identifier))
def visit_Expr(self, expr: ast.Expr): """ Visitor of the atom expression node :param expr: """ value = self.visit(expr.value) if isinstance(expr.value, ast.Name): if ( # it is not a symbol of the method scope (self._current_method is not None and value not in self._current_method.symbols) # nor it is a symbol of the module scope or (self._current_module is not None and value not in self._current_module.symbols) # nor it is a symbol of the global scope or value not in self.symbols ): self._log_error( CompilerError.UnresolvedReference(expr.value.lineno, expr.value.col_offset, value) )
def validate_passed_arguments(self, call: ast.Call, args_types: List[IType], callable_id: str, callable: Callable): if isinstance(callable, IBuiltinMethod): builtin_analyser = BuiltinFunctionCallAnalyser(self, call, callable_id, callable) if builtin_analyser.validate(): self.errors.extend(builtin_analyser.errors) self.warnings.extend(builtin_analyser.warnings) return for index, (arg_id, arg_value) in enumerate(callable.args.items()): param = call.args[index] param_type = self.get_type(param) args_types.append(param_type) if not arg_value.type.is_type_of(param_type): self._log_error( CompilerError.MismatchedTypes( param.lineno, param.col_offset, arg_value.type.identifier, param_type.identifier ))
def get_un_op(self, operator: Operator, operand: Any) -> UnaryOperation: """ Returns the binary operation specified by the operator and the types of the operands :param operator: the operator :param operand: the operand :return: Returns the corresponding :class:`UnaryOperation` if the types are valid. :raise MismatchedTypes: raised if the types aren't valid for the operator """ op_type: IType = self.get_type(operand) actual_type: str = op_type.identifier operation: UnaryOperation = UnaryOp.validate_type(operator, op_type) if operation is not None: return operation else: expected_op: UnaryOperation = UnaryOp.get_operation_by_operator(operator) expected_type: str = expected_op.operand_type.identifier raise CompilerError.MismatchedTypes(0, 0, expected_type, actual_type)
def visit_Call(self, call: ast.Call) -> Optional[IType]: """ Visitor of a function call node :param call: the python ast function call node :return: the result type of the called function. None if the function is not found """ func_id = self.visit(call.func) func_symbol = self.get_symbol(func_id) # if func_symbol is None, the called function may be a function written after in the code # that's why it shouldn't log a compiler error here if func_symbol is None: return None if not isinstance(func_symbol, Callable): # verifiy if it is a builtin method with its name shadowed func = Builtin.get_symbol(func_id) func_symbol = func if func is not None else func_symbol if func_symbol is Type.exception: func_symbol = Builtin.Exception elif isinstance(func_symbol, ClassType): func_symbol = func_symbol.constructor_method() if not isinstance(func_symbol, Callable): # the symbol doesn't exists self._log_error( CompilerError.UnresolvedReference(call.func.lineno, call.func.col_offset, func_id)) elif isinstance(func_symbol, IBuiltinMethod): if func_symbol.body is not None: self._builtin_functions_to_visit[func_id] = func_symbol elif func_symbol is Builtin.NewEvent: new_event = self.create_new_event(call) self.__include_callable(new_event.identifier, new_event) self._current_event = new_event return self.get_type(call.func)
def visit_While(self, while_node: ast.While): """ Verifies if the type of while test is valid :param while_node: the python ast while statement node """ test = self.visit(while_node.test) test_type: IType = self.get_type(test) if test_type is not Type.bool: self._log_error( CompilerError.MismatchedTypes( while_node.lineno, while_node.col_offset, actual_type_id=test_type.identifier, expected_type_id=Type.bool.identifier) ) # continue to walk through the tree for stmt in while_node.body: self.visit(stmt) for stmt in while_node.orelse: self.visit(stmt)
def update_callable_after_validation(self, call: ast.Call, callable_id: str, callable_target: Callable): # if the arguments are not generic, include the specified method in the symbol table if (isinstance(callable_target, IBuiltinMethod) and callable_target.identifier != callable_id and callable_target.raw_identifier == callable_id and callable_target.identifier not in self.symbols): self.symbols[callable_target.identifier] = callable_target call.func = ast.Name(lineno=call.func.lineno, col_offset=call.func.col_offset, ctx=ast.Load(), id=callable_target.identifier) # validates if metadata matches callable's requirements if hasattr(callable_target, 'requires_storage') and callable_target.requires_storage: if not self._metadata.has_storage: self._log_error( CompilerError.MetadataInformationMissing( line=call.func.lineno, col=call.func.col_offset, symbol_id=callable_target.identifier, metadata_attr_id='has_storage' ) ) else: self._current_method.set_storage()
def visit_FunctionDef(self, function: ast.FunctionDef): """ Visitor of the function node Performs the type checking in the body of the function :param function: the python ast function definition node """ self.visit(function.args) method = self.symbols[function.name] from boa3.model.event import Event if isinstance(method, Method): self._current_method = method for stmt in function.body: self.visit(stmt) self._validate_return(function) if (len(function.body) > 0 and not isinstance(function.body[-1], ast.Return) and method.return_type is Type.none): # include return None in void functions default_value: str = str(method.return_type.default_value) node: ast.AST = ast.parse(default_value).body[0].value function.body.append( ast.Return(lineno=function.lineno, col_offset=function.col_offset, value=node) ) self._current_method = None elif (isinstance(method, Event) # events don't have return and function.returns is not None): return_type = self.get_type(function.returns) if return_type is not Type.none: self._log_error( CompilerError.MismatchedTypes(line=function.lineno, col=function.col_offset, expected_type_id=Type.none.identifier, actual_type_id=return_type.identifier) )
def _read_metadata_object(self, function: ast.FunctionDef): """ Gets the metadata object defined in this function :param function: """ if self._metadata is not None: # metadata function has been defined already self._log_warning( CompilerWarning.RedeclaredSymbol( line=function.lineno, col=function.col_offset, symbol_id=Builtin.Metadata.identifier)) # this function must have a return and no arguments elif len(function.args.args) != 0: self._log_error( CompilerError.UnexpectedArgument(line=function.lineno, col=function.col_offset)) elif not any(isinstance(stmt, ast.Return) for stmt in function.body): self._log_error( CompilerError.MissingReturnStatement(line=function.lineno, col=function.col_offset, symbol_id=function.name)) else: function.returns = None function.decorator_list = [] module: ast.Module = ast.parse('') module.body = [ node for node in self._tree.body if isinstance(node, (ast.ImportFrom, ast.Import)) ] module.body.append(function) ast.copy_location(module, function) # executes the function code = compile(module, filename='<boa3>', mode='exec') namespace = {} exec(code, namespace) obj: Any = namespace[function.name]() node: ast.AST = function.body[-1] if len( function.body) > 0 else function # return must be a NeoMetadata object if not isinstance(obj, NeoMetadata): obj_type = self.get_type(obj).identifier if self.get_type( obj) is not Type.any else type(obj).__name__ self._log_error( CompilerError.MismatchedTypes( line=node.lineno, col=node.col_offset, expected_type_id=NeoMetadata.__name__, actual_type_id=obj_type)) return # validates the metadata attributes types attributes: Dict[str, Any] = { attr: value for attr, value in dict(obj.__dict__).items() if attr in Builtin.metadata_fields } if any(not isinstance(value, Builtin.metadata_fields[attr]) for attr, value in attributes.items()): for expected, actual in [ (Builtin.metadata_fields[attr], type(v_type)) for attr, v_type in attributes.items() if not isinstance(v_type, Builtin.metadata_fields[attr]) ]: if isinstance(expected, Iterable): expected_id = 'Union[{0}]'.format(', '.join( [tpe.__name__ for tpe in expected])) elif hasattr(expected, '__name__'): expected_id = expected.__name__ else: expected_id = str(expected) self._log_error( CompilerError.MismatchedTypes( line=node.lineno, col=node.col_offset, expected_type_id=expected_id, actual_type_id=actual.__name__)) else: # if the function was defined correctly, sets the metadata object of the smart contract self._metadata = obj self._metadata_node = function # for error messages only
def _log_unresolved_import(self, origin_node: ast.AST, import_id: str): if self._log: self._log_error( CompilerError.UnresolvedReference(line=origin_node.lineno, col=origin_node.col_offset, symbol_id=import_id))
def visit_ClassDef(self, node: ast.ClassDef): # TODO: refactor when classes defined by the user are implemented self._log_error( CompilerError.NotSupportedOperation(node.lineno, node.col_offset, symbol_id='class'))
def _validate_standards(self): for standard in self.standards: if standard in supportedstandard.neo_standards: current_standard = supportedstandard.neo_standards[standard] # validate standard's methods for standard_method in current_standard.methods: method_id = standard_method.external_name is_implemented = False found_methods = self.get_methods_by_display_name(method_id) for method in found_methods: if isinstance( method, Method) and current_standard.match_definition( standard_method, method): is_implemented = True break if not is_implemented: self._log_error( CompilerError.MissingStandardDefinition( standard, method_id, standard_method)) # validate standard's events events = [ symbol for symbol in self.symbols.values() if isinstance(symbol, Event) ] # imported events should be included in the validation for imported in self._get_all_imports(): events.extend([ event for event in imported.all_symbols.values() if isinstance(event, Event) ]) for standard_event in current_standard.events: is_implemented = False for event in events: if (event.name == standard_event.name and current_standard.match_definition( standard_event, event)): is_implemented = True break if not is_implemented: self._log_error( CompilerError.MissingStandardDefinition( standard, standard_event.name, standard_event)) # validate optional methods for optional_method in current_standard.optionals: method_id = optional_method.external_name is_implemented = False found_methods = self.get_methods_by_display_name(method_id) for method in found_methods: if isinstance( method, Method) and current_standard.match_definition( optional_method, method): is_implemented = True break if found_methods and not is_implemented: self._log_error( CompilerError.MissingStandardDefinition( standard, method_id, optional_method))
def validate_values(self, *params: Any) -> List[Any]: values = [] if len(params) != 2: return values origin, visitor = params values.append(self.external_name) from boa3.analyser.astanalyser import IAstAnalyser if not isinstance(visitor, IAstAnalyser): return values from boa3.exception import CompilerError if not isinstance(origin, ast.Call): visitor._log_error( CompilerError.UnfilledArgument(origin.lineno, origin.col_offset, list(self.args.keys())[0])) return values args_names = list(self.args.keys()) if len(origin.args) > len(args_names): visitor._log_error( CompilerError.UnexpectedArgument(origin.lineno, origin.col_offset)) return values # read the called arguments args_len = min(len(origin.args), len(args_names)) values.clear() for x in range(args_len): values.append(visitor.visit(origin.args[x])) if len(values) < 1: values.append(self.external_name) # read the called keyword arguments for kwarg in origin.keywords: if kwarg.arg in args_names: x = args_names.index(kwarg.arg) value = visitor.visit(kwarg.value) values[x] = value else: visitor._log_error( CompilerError.UnexpectedArgument(kwarg.lineno, kwarg.col_offset)) if not isinstance(values[0], str): visitor._log_error( CompilerError.UnfilledArgument(origin.lineno, origin.col_offset, list(self.args.keys())[0])) return values if visitor.get_symbol(values[0]) is not None: visitor._log_error( CompilerError.InvalidUsage( origin.lineno, origin.col_offset, "Only literal values are accepted for 'name' argument")) values[0] = self.external_name return values