diff --git a/tests/unit/ast/test_natspec.py b/tests/unit/ast/test_natspec.py index 0b860e562e..710b7a9312 100644 --- a/tests/unit/ast/test_natspec.py +++ b/tests/unit/ast/test_natspec.py @@ -1,6 +1,7 @@ import pytest from vyper import ast as vy_ast +from vyper.compiler import compile_code from vyper.compiler.phases import CompilerData from vyper.exceptions import NatSpecSyntaxException @@ -65,10 +66,10 @@ def parse_natspec(code): def test_documentation_example_output(): - userdoc, devdoc = parse_natspec(test_code) + natspec = parse_natspec(test_code) - assert userdoc == expected_userdoc - assert devdoc == expected_devdoc + assert natspec.userdoc == expected_userdoc + assert natspec.devdoc == expected_devdoc def test_no_tags_implies_notice(): @@ -84,13 +85,13 @@ def foo(): pass """ - userdoc, devdoc = parse_natspec(code) + natspec = parse_natspec(code) - assert userdoc == { + assert natspec.userdoc == { "methods": {"foo()": {"notice": "This one too!"}}, "notice": "Because there is no tag, this docstring is handled as a notice.", } - assert not devdoc + assert natspec.devdoc == {} def test_whitespace(): @@ -111,9 +112,9 @@ def test_whitespace(): @author Mr No-linter ''' """ - _, devdoc = parse_natspec(code) + natspec = parse_natspec(code) - assert devdoc == { + assert natspec.devdoc == { "author": "Mr No-linter", "details": "Whitespace gets cleaned up, people can use awful formatting. We don't mind!", } @@ -131,9 +132,9 @@ def foo(bar: int128, baz: uint256, potato: bytes32): pass """ - _, devdoc = parse_natspec(code) + natspec = parse_natspec(code) - assert devdoc == { + assert natspec.devdoc == { "methods": { "foo(int128,uint256,bytes32)": { "details": "we didn't document potato, but that's ok", @@ -154,9 +155,9 @@ def foo(bar: int128, baz: uint256) -> (int128, uint256): return bar, baz """ - _, devdoc = parse_natspec(code) + natspec = parse_natspec(code) - assert devdoc == { + assert natspec.devdoc == { "methods": { "foo(int128,uint256)": {"returns": {"_0": "value of bar", "_1": "value of baz"}} } @@ -176,9 +177,9 @@ def notfoo(bar: int128, baz: uint256): pass """ - _, devdoc = parse_natspec(code) + natspec = parse_natspec(code) - assert devdoc["methods"] == {"foo(int128,uint256)": {"details": "I will be parsed."}} + assert natspec.devdoc["methods"] == {"foo(int128,uint256)": {"details": "I will be parsed."}} def test_partial_natspec(): @@ -276,9 +277,9 @@ def foo(): pass """ - _, devdoc = parse_natspec(code) + natspec = parse_natspec(code) - assert devdoc == {"license": license} + assert natspec.devdoc == {"license": license} fields = ["title", "author", "license", "notice", "dev"] @@ -417,3 +418,21 @@ def foo() -> (int128,uint256): NatSpecSyntaxException, match="Number of documented return values exceeds actual number" ): parse_natspec(code) + + +def test_natspec_parsed_implicitly(): + # test natspec is parsed even if not explicitly requested + code = """ +''' +@noticee x +''' + """ + with pytest.raises(NatSpecSyntaxException): + parse_natspec(code) + + # check we can get ast + compile_code(code, output_formats=["ast_dict"]) + + # anything beyond ast is blocked + with pytest.raises(NatSpecSyntaxException): + compile_code(code, output_formats=["annotated_ast_dict"]) diff --git a/vyper/ast/natspec.py b/vyper/ast/natspec.py index 41a6703b6e..48fc9134dd 100644 --- a/vyper/ast/natspec.py +++ b/vyper/ast/natspec.py @@ -1,4 +1,5 @@ import re +from dataclasses import dataclass from typing import Optional, Tuple from asttokens import LineNumbers @@ -11,7 +12,13 @@ USERDOCS_FIELDS = ("notice",) -def parse_natspec(annotated_vyper_module: vy_ast.Module) -> Tuple[dict, dict]: +@dataclass +class NatspecOutput: + userdoc: dict + devdoc: dict + + +def parse_natspec(annotated_vyper_module: vy_ast.Module) -> NatspecOutput: """ Parses NatSpec documentation from a contract. @@ -63,7 +70,7 @@ def parse_natspec(annotated_vyper_module: vy_ast.Module) -> Tuple[dict, dict]: if fn_natspec: devdoc.setdefault("methods", {})[method_id] = fn_natspec - return userdoc, devdoc + return NatspecOutput(userdoc=userdoc, devdoc=devdoc) def _parse_docstring( diff --git a/vyper/compiler/output.py b/vyper/compiler/output.py index f8beb9d11b..9b3bd147ef 100644 --- a/vyper/compiler/output.py +++ b/vyper/compiler/output.py @@ -2,7 +2,7 @@ from collections import deque from pathlib import PurePath -from vyper.ast import ast_to_dict, parse_natspec +from vyper.ast import ast_to_dict from vyper.codegen.ir_node import IRnode from vyper.compiler.phases import CompilerData from vyper.compiler.utils import build_gas_estimates @@ -30,13 +30,11 @@ def build_annotated_ast_dict(compiler_data: CompilerData) -> dict: def build_devdoc(compiler_data: CompilerData) -> dict: - userdoc, devdoc = parse_natspec(compiler_data.annotated_vyper_module) - return devdoc + return compiler_data.natspec.devdoc def build_userdoc(compiler_data: CompilerData) -> dict: - userdoc, devdoc = parse_natspec(compiler_data.annotated_vyper_module) - return userdoc + return compiler_data.natspec.userdoc def build_external_interface_output(compiler_data: CompilerData) -> str: diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py index a9a003f80a..e1ee91df72 100644 --- a/vyper/compiler/phases.py +++ b/vyper/compiler/phases.py @@ -5,6 +5,7 @@ from typing import Optional from vyper import ast as vy_ast +from vyper.ast import natspec from vyper.codegen import module from vyper.codegen.ir_node import IRnode from vyper.compiler.input_bundle import FileInput, FilesystemInputBundle, InputBundle @@ -150,9 +151,19 @@ def _generate_ast(self): def vyper_module(self): return self._generate_ast + @cached_property + def _annotate(self) -> tuple[natspec.NatspecOutput, vy_ast.Module]: + module = generate_annotated_ast(self.vyper_module, self.input_bundle) + nspec = natspec.parse_natspec(module) + return nspec, module + + @cached_property + def natspec(self) -> natspec.NatspecOutput: + return self._annotate[0] + @cached_property def annotated_vyper_module(self) -> vy_ast.Module: - return generate_annotated_ast(self.vyper_module, self.input_bundle) + return self._annotate[1] @cached_property def compilation_target(self): @@ -173,6 +184,8 @@ def storage_layout(self) -> StorageLayout: def global_ctx(self) -> ModuleT: # ensure storage layout is computed _ = self.storage_layout + # ensure natspec is computed + _ = self.natspec return self.annotated_vyper_module._metadata["type"] @cached_property