def test_params(): code = """ @public def foo(bar: int128, baz: uint256, potato: bytes32): ''' @param bar a number @param baz also a number @dev we didn't document potato, but that's ok ''' pass """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) _, devdoc = parse_natspec(vyper_ast, global_ctx) assert devdoc == { "methods": { "foo(int128,uint256,bytes32)": { "details": "we didn't document potato, but that's ok", "params": { "bar": "a number", "baz": "also a number" }, } } }
def test_returns(): code = """ @public def foo(bar: int128, baz: uint256) -> (int128, uint256): ''' @return value of bar @return value of baz ''' return bar, baz """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) _, devdoc = parse_natspec(vyper_ast, global_ctx) assert devdoc == { "methods": { "foo(int128,uint256)": { "returns": { "_0": "value of bar", "_1": "value of baz" } } } }
def parse_tree_to_lll(code, origcode, runtime_only=False): global_ctx = GlobalContext.get_global_context(code) _names_def = [_def.name for _def in global_ctx._defs] # Checks for duplicate function names if len(set(_names_def)) < len(_names_def): raise FunctionDeclarationException("Duplicate function name: %s" % [name for name in _names_def if _names_def.count(name) > 1][0]) _names_events = [_event.target.id for _event in global_ctx._events] # Checks for duplicate event names if len(set(_names_events)) < len(_names_events): raise EventDeclarationException("Duplicate event name: %s" % [name for name in _names_events if _names_events.count(name) > 1][0]) # Initialization function initfunc = [_def for _def in global_ctx._defs if is_initializer(_def)] # Default function defaultfunc = [_def for _def in global_ctx._defs if is_default_func(_def)] # Regular functions otherfuncs = [_def for _def in global_ctx._defs if not is_initializer(_def) and not is_default_func(_def)] sigs = {} external_contracts = {} # Create the main statement o = ['seq'] if global_ctx._events: sigs = parse_events(sigs, global_ctx) if global_ctx._contracts: external_contracts = parse_external_contracts(external_contracts, global_ctx._contracts, global_ctx._structs, global_ctx._constants) # If there is an init func... if initfunc: o.append(['seq', initializer_lll]) o.append(parse_func(initfunc[0], {**{'self': sigs}, **external_contracts}, origcode, global_ctx)) # If there are regular functions... if otherfuncs or defaultfunc: o = parse_other_functions( o, otherfuncs, sigs, external_contracts, origcode, global_ctx, defaultfunc, runtime_only ) return LLLnode.from_list(o, typ=None)
def test_documentation_example_output(): vyper_ast = parse_to_ast(test_code) global_ctx = GlobalContext.get_global_context(vyper_ast) userdoc, devdoc = parse_natspec(vyper_ast, global_ctx) assert userdoc == expected_userdoc assert devdoc == expected_devdoc
def test_no_tags_implies_notice(): code = """ ''' Because there is no tag, this docstring is handled as a notice. ''' @public def foo(): ''' This one too! ''' pass """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) userdoc, devdoc = parse_natspec(vyper_ast, global_ctx) assert userdoc == { "methods": { "foo()": { "notice": "This one too!" } }, "notice": "Because there is no tag, this docstring is handled as a notice.", } assert not devdoc
def mk_full_signature(code, sig_formatter=None, interface_codes=None): if sig_formatter is None: # Use default JSON style output. sig_formatter = _default_sig_formatter o = [] global_ctx = GlobalContext.get_global_context( code, interface_codes=interface_codes) # Produce event signatues. for code in global_ctx._events: sig = EventSignature.from_declaration(code, global_ctx) o.append(sig_formatter(sig, global_ctx._custom_units_descriptions)) # Produce function signatures. for code in global_ctx._defs: sig = FunctionSignature.from_definition( code, sigs=global_ctx._contracts, custom_units=global_ctx._custom_units, custom_structs=global_ctx._structs, constants=global_ctx._constants) if not sig.private: default_sigs = generate_default_arg_sigs(code, global_ctx._contracts, global_ctx) for s in default_sigs: o.append( sig_formatter(s, global_ctx._custom_units_descriptions)) return o
def test_whitespace(): code = """ ''' @dev Whitespace gets cleaned up, people can use awful formatting. We don't mind! @author Mr No-linter ''' """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) _, devdoc = parse_natspec(vyper_ast, global_ctx) assert devdoc == { "author": "Mr No-linter", "details": "Whitespace gets cleaned up, people can use awful formatting. We don't mind!", }
def parse_natspec( vyper_ast: vy_ast.Module, interface_codes: Union[InterfaceDict, InterfaceImports, None] = None, ) -> Tuple[dict, dict]: """ Parses NatSpec documentation from a contract. Arguments --------- vyper_ast : Module Module-level vyper ast node. interface_codes: Dict, optional Dict containing relevant data for any import statements related to this contract. Returns ------- dict NatSpec user documentation dict NatSpec developer documentation """ userdoc, devdoc = {}, {} source: str = vyper_ast.full_source_code global_ctx = GlobalContext.get_global_context(vyper_ast, interface_codes) docstring = vyper_ast.get("doc_string.value") if docstring: devdoc.update(_parse_docstring(source, docstring, ("param", "return"))) if "notice" in devdoc: userdoc["notice"] = devdoc.pop("notice") for node in [i for i in vyper_ast.body if i.get("doc_string.value")]: docstring = node.doc_string.value sigs = sig_utils.mk_single_method_identifier(node, global_ctx) if isinstance(node.returns, vy_ast.Tuple): ret_len = len(node.returns.elts) elif node.returns: ret_len = 1 else: ret_len = 0 if sigs: args = tuple(i.arg for i in node.args.args) fn_natspec = _parse_docstring(source, docstring, ("title",), args, ret_len) for s in sigs: if "notice" in fn_natspec: userdoc.setdefault("methods", {})[s] = { "notice": fn_natspec.pop("notice") } if fn_natspec: devdoc.setdefault("methods", {})[s] = fn_natspec return userdoc, devdoc
def mk_method_identifiers(source_str, interface_codes=None): identifiers = {} global_ctx = GlobalContext.get_global_context( parse_to_ast(source_str), interface_codes=interface_codes, ) for code in global_ctx._defs: identifiers.update(mk_single_method_identifier(code, global_ctx)) return identifiers
def parse_to_lll( source_code: str, runtime_only: bool = False, interface_codes: Optional[InterfaceImports] = None ) -> LLLnode: vyper_module = vy_ast.parse_to_ast(source_code) global_ctx = GlobalContext.get_global_context(vyper_module, interface_codes=interface_codes) lll_nodes, lll_runtime = parse_tree_to_lll(source_code, global_ctx) if runtime_only: return lll_runtime else: return lll_nodes
def test_empty_param(bad_docstring): code = f""" @external def foo(a: int128): '''{bad_docstring}''' pass """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) with pytest.raises(NatSpecSyntaxException, match="No description given for parameter 'a'"): parse_natspec(vyper_ast, global_ctx)
def test_empty_field(bad_docstring): code = f""" @public def foo(): '''{bad_docstring}''' pass """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) with pytest.raises(NatSpecSyntaxException, match="No description given for tag '@notice'"): parse_natspec(vyper_ast, global_ctx)
def test_invalid_field(field): code = f""" @external def foo(): '''@{field} function level docstrings cannot have titles''' pass """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) with pytest.raises(NatSpecSyntaxException, match=f"'@{field}' is not a valid field"): parse_natspec(vyper_ast, global_ctx)
def mk_method_identifiers(code): o = {} global_ctx = GlobalContext.get_global_context(parse(code)) for code in global_ctx._defs: sig = FunctionSignature.from_definition(code, sigs=global_ctx._contracts, custom_units=global_ctx._custom_units, constants=global_ctx._constants) if not sig.private: default_sigs = generate_default_arg_sigs(code, global_ctx._contracts, global_ctx) for s in default_sigs: o[s.sig] = hex(s.method_id) return o
def test_unknown_param(): code = """ @external def foo(bar: int128, baz: uint256): '''@param hotdog not a number''' pass """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) with pytest.raises(NatSpecSyntaxException, match="Method has no parameter 'hotdog'"): parse_natspec(vyper_ast, global_ctx)
def test_too_many_returns_no_return_type(): code = """ @external def foo(): '''@return should fail, the function does not include a return value''' pass """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) with pytest.raises(NatSpecSyntaxException, match="Method does not return any values"): parse_natspec(vyper_ast, global_ctx)
def test_empty_fields(field): code = f""" ''' @{field} ''' @external def foo(): pass """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) with pytest.raises(NatSpecSyntaxException, match=f"No description given for tag '@{field}'"): parse_natspec(vyper_ast, global_ctx)
def mk_full_signature(code): o = [] global_ctx = GlobalContext.get_global_context(code) for code in global_ctx._events: sig = EventSignature.from_declaration(code, custom_units=global_ctx._custom_units) o.append(sig.to_abi_dict()) for code in global_ctx._defs: sig = FunctionSignature.from_definition(code, sigs=global_ctx._contracts, custom_units=global_ctx._custom_units) if not sig.private: default_sigs = generate_default_arg_sigs(code, global_ctx._contracts, global_ctx._custom_units) for s in default_sigs: o.append(s.to_abi_dict()) return o
def produce_source_map(code, interface_codes=None): global_ctx = GlobalContext.get_global_context( parser.parse_to_ast(code), interface_codes=interface_codes) asm_list = compile_lll.compile_to_assembly( optimizer.optimize( parse_to_lll(code, runtime_only=True, interface_codes=interface_codes))) c, line_number_map = compile_lll.assembly_to_evm(asm_list) source_map = { "globals": {}, "locals": {}, "line_number_map": line_number_map } source_map["globals"] = { name: serialise_var_rec(var_record) for name, var_record in global_ctx._globals.items() } # Fetch context for each function. lll = parser.parse_tree_to_lll( parser.parse_to_ast(code), code, interface_codes=interface_codes, runtime_only=True, ) contexts = { f.func_name: f.context for f in lll.args[1:] if hasattr(f, "context") } prev_func_name = None for _def in global_ctx._defs: if _def.name != "__init__": func_info = {"from_lineno": _def.lineno, "variables": {}} # set local variables for specific function. context = contexts[_def.name] func_info["variables"] = { var_name: serialise_var_rec(var_rec) for var_name, var_rec in context.vars.items() } source_map["locals"][_def.name] = func_info # set to_lineno if prev_func_name: source_map["locals"][prev_func_name]["to_lineno"] = _def.lineno prev_func_name = _def.name source_map["locals"][_def.name]["to_lineno"] = len(code.splitlines()) return source_map
def test_unknown_field(): code = """ @external def foo(): ''' @notice this is ok @thing this is bad ''' pass """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) with pytest.raises(NatSpecSyntaxException, match="Unknown NatSpec field '@thing'"): parse_natspec(vyper_ast, global_ctx)
def test_duplicate_fields(): code = """ @external def foo(): ''' @notice It's fine to have one notice, but.... @notice a second one, not so much ''' pass """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) with pytest.raises(NatSpecSyntaxException, match="Duplicate NatSpec field '@notice'"): parse_natspec(vyper_ast, global_ctx)
def test_duplicate_param(): code = """ @external def foo(bar: int128, baz: uint256): ''' @param bar a number @param bar also a number ''' pass """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) with pytest.raises(NatSpecSyntaxException, match="Parameter 'bar' documented more than once"): parse_natspec(vyper_ast, global_ctx)
def test_partial_natspec(): code = """ @public def foo(): ''' Regular comments preceeding natspec is not allowed @notice this is natspec ''' pass """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) with pytest.raises(NatSpecSyntaxException, match="NatSpec docstring opens with untagged comment"): parse_natspec(vyper_ast, global_ctx)
def extract_sigs(sig_code): if sig_code['type'] == 'vyper': interface_ast = [ i for i in vy_ast.parse_to_ast(sig_code['code']) if isinstance(i, vy_ast.FunctionDef) or (isinstance(i, vy_ast.AnnAssign) and i.target.id != "implements") ] global_ctx = GlobalContext.get_global_context(interface_ast) return sig_utils.mk_full_signature(global_ctx, sig_formatter=lambda x: x) elif sig_code['type'] == 'json': return mk_full_signature_from_json(sig_code['code']) else: raise Exception(( f"Unknown interface signature type '{sig_code['type']}' supplied. " "'vyper' & 'json' are supported"))
def test_license(license): code = f""" ''' @license {license} ''' @external def foo(): pass """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) _, devdoc = parse_natspec(vyper_ast, global_ctx) assert devdoc == { "license": license, }
def test_too_many_returns_single_return_type(): code = """ @external def foo() -> int128: ''' @return int128 @return this should fail ''' return 1 """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) with pytest.raises( NatSpecSyntaxException, match="Number of documented return values exceeds actual number", ): parse_natspec(vyper_ast, global_ctx)
def test_ignore_private_methods(): code = """ @external def foo(bar: int128, baz: uint256): '''@dev I will be parsed.''' pass @internal def notfoo(bar: int128, baz: uint256): '''@dev I will not be parsed.''' pass """ vyper_ast = parse_to_ast(code) global_ctx = GlobalContext.get_global_context(vyper_ast) _, devdoc = parse_natspec(vyper_ast, global_ctx) assert devdoc["methods"] == {"foo(int128,uint256)": {"details": "I will be parsed."}}
def produce_source_map(code): global_ctx = GlobalContext.get_global_context(parser.parse_to_ast(code)) asm_list = compile_lll.compile_to_assembly( optimizer.optimize(parse_to_lll(code, runtime_only=True))) c, line_number_map = compile_lll.assembly_to_evm(asm_list) source_map = { 'globals': {}, 'locals': {}, 'line_number_map': line_number_map } source_map['globals'] = { name: serialise_var_rec(var_record) for name, var_record in global_ctx._globals.items() } # Fetch context for each function. lll = parser.parse_tree_to_lll(parser.parse_to_ast(code), code, runtime_only=True) contexts = { f.func_name: f.context for f in lll.args[1:] if hasattr(f, 'context') } prev_func_name = None for _def in global_ctx._defs: if _def.name != '__init__': func_info = {'from_lineno': _def.lineno, 'variables': {}} # set local variables for specific function. context = contexts[_def.name] func_info['variables'] = { var_name: serialise_var_rec(var_rec) for var_name, var_rec in context.vars.items() } source_map['locals'][_def.name] = func_info # set to_lineno if prev_func_name: source_map['locals'][prev_func_name]['to_lineno'] = _def.lineno prev_func_name = _def.name source_map['locals'][_def.name]['to_lineno'] = len(code.splitlines()) return source_map
def generate_global_context( vyper_module: vy_ast.Module, interface_codes: Optional[InterfaceImports], ) -> GlobalContext: """ Generate a contextualized AST from the Vyper AST. Arguments --------- vyper_module : vy_ast.Module Top-level Vyper AST node interface_codes: Dict, optional Interfaces that may be imported by the contracts. Returns ------- GlobalContext Sorted, contextualized representation of the Vyper AST """ return GlobalContext.get_global_context(vyper_module, interface_codes=interface_codes)
def mk_method_identifiers(code): o = [] global_ctx = GlobalContext.get_global_context(parse(code)) for code in global_ctx._defs: sig = FunctionSignature.from_definition( code, sigs=global_ctx._contracts, custom_units=global_ctx._custom_units) if not sig.private: default_sigs = generate_default_arg_sigs(code, global_ctx._contracts, global_ctx._custom_units) for s in default_sigs: o.append( s.get_method_identifier(global_ctx._contracts, global_ctx._custom_units)) return dict(o)