Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix[ux]: add missing filename to syntax exceptions #4343

Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4425feb
add path info to syntax exception
sandbubbles Nov 1, 2024
3316865
set path for natspec exceptions
sandbubbles Nov 1, 2024
90723e1
lint
sandbubbles Nov 2, 2024
bdf5a42
rewrite the path attribute
sandbubbles Nov 2, 2024
363cf1d
test natspec exception contains file
sandbubbles Nov 2, 2024
241511b
test version exception contains filename
sandbubbles Nov 2, 2024
b3c8ef5
test imported invalid compiler version is attributed correctly
sandbubbles Nov 2, 2024
02c5765
check syntax exception in json contains filename
sandbubbles Nov 2, 2024
548d583
check the exception is attributed to the correct file
sandbubbles Nov 2, 2024
67dcfb8
test syntax exceptions report file correctly
sandbubbles Nov 2, 2024
2dded1a
fix isort errors
sandbubbles Nov 4, 2024
2dfa033
refactor[ux]: add `venom` as `experimental-codegen` alias (#4337)
sandbubbles Nov 5, 2024
5637913
fix[ci]: fix README encoding in `setup.py` (#4348)
charles-cooper Nov 5, 2024
b6591b4
feat[docs]: add Telegram badge to README.md (#4342)
rafael-abuawad Nov 5, 2024
9450428
extract adding contract info into a function
sandbubbles Nov 7, 2024
97b3abe
update error message verification to include line number
sandbubbles Nov 7, 2024
1b0e02a
Merge branch 'master' into fix/syntax-exception-missing-filename
charles-cooper Nov 19, 2024
05e24d6
wrap parse_natspec in a try catch to add path
sandbubbles Nov 21, 2024
5101a27
remove changes to _annotate functions
sandbubbles Nov 21, 2024
7f44b10
rename method to "_format_contract_details"
sandbubbles Nov 21, 2024
88174e7
lint
sandbubbles Nov 21, 2024
e66da98
remove redundant "pass"
sandbubbles Nov 21, 2024
bf3ee5b
change path to resolved_path in exceptions
sandbubbles Nov 21, 2024
bd0d2d3
wrap path in safe_relpath
sandbubbles Nov 21, 2024
b6adc0b
lint
sandbubbles Nov 21, 2024
43edf77
add chdir_tmp_path to test fixtures and remove wildcards
sandbubbles Nov 21, 2024
96bac14
remove chdir_tmp_path when make_input_bundle is not used
sandbubbles Nov 21, 2024
af391a6
check resolved_path is not unknown
sandbubbles Dec 7, 2024
c09dc65
use only the msg of SyntaxError
sandbubbles Dec 7, 2024
6ca0cbb
Merge branch 'master' into fix/syntax-exception-missing-filename
sandbubbles Dec 7, 2024
fd8d78d
adjust the offset by 1
sandbubbles Dec 7, 2024
78362e6
test offset points to correct place
sandbubbles Dec 7, 2024
5ea4e2c
add comment
sandbubbles Dec 7, 2024
5022cfd
nits
charles-cooper Dec 13, 2024
7f0b3f4
fix lint
charles-cooper Dec 13, 2024
531cdca
move comment
charles-cooper Dec 13, 2024
8e87405
Merge branch 'master' into fix/syntax-exception-missing-filename
charles-cooper Dec 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion tests/functional/syntax/exceptions/test_vyper_exception_pos.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pytest import raises

from vyper.exceptions import VyperException
from vyper import compile_code
from vyper.exceptions import SyntaxException, VyperException


def test_type_exception_pos():
Expand Down Expand Up @@ -29,3 +30,32 @@ def __init__():

"""
assert_compile_failed(lambda: get_contract(code), VyperException)


def test_exception_contains_file(make_input_bundle):
code = """
def bar()>:
"""
input_bundle = make_input_bundle({"code.vy": code})
with raises(SyntaxException, match="contract"):
compile_code(code, input_bundle=input_bundle)


def test_exception_reports_correct_file(make_input_bundle, chdir_tmp_path):
code_a = "def bar()>:"
code_b = "import A"
input_bundle = make_input_bundle({"A.vy": code_a, "B.vy": code_b})

with raises(SyntaxException, match=r'contract "A\.vy:\d+"'):
compile_code(code_b, input_bundle=input_bundle)


def test_syntax_exception_reports_correct_offset(make_input_bundle):
code = """
def foo():
uint256 a = pass
"""
input_bundle = make_input_bundle({"code.vy": code})

with raises(SyntaxException, match="line \d+:12"):
compile_code(code, input_bundle=input_bundle)
16 changes: 16 additions & 0 deletions tests/unit/ast/test_natspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,19 @@ def test_natspec_parsed_implicitly():
# anything beyond ast is blocked
with pytest.raises(NatSpecSyntaxException):
compile_code(code, output_formats=["annotated_ast_dict"])


def test_natspec_exception_contains_file_path():
code = """
@external
def foo() -> (int128,uint256):
'''
@return int128
@return uint256
@return this should fail
'''
return 1, 2
"""

with pytest.raises(NatSpecSyntaxException, match=r'contract "VyperContract\.vy:\d+"'):
parse_natspec(code)
20 changes: 20 additions & 0 deletions tests/unit/ast/test_pre_parser.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from pathlib import Path

import pytest

from vyper import compile_code
Expand Down Expand Up @@ -56,6 +58,24 @@ def test_invalid_version_pragma(file_version, mock_version):
validate_version_pragma(f"{file_version}", file_version, (SRC_LINE))


def test_invalid_version_contains_file(mock_version):
mock_version(COMPILER_VERSION)
with pytest.raises(VersionException, match=r'contract "mock\.vy:\d+"'):
compile_code("# pragma version ^0.3.10", resolved_path=Path("mock.vy"))


def test_imported_invalid_version_contains_correct_file(
mock_version, make_input_bundle, chdir_tmp_path
):
code_a = "# pragma version ^0.3.10"
code_b = "import A"
input_bundle = make_input_bundle({"A.vy": code_a, "B.vy": code_b})
mock_version(COMPILER_VERSION)

with pytest.raises(VersionException, match=r'contract "A\.vy:\d+"'):
compile_code(code_b, input_bundle=input_bundle)


prerelease_valid_versions = [
"<0.1.1-beta.9",
"<0.1.1b9",
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/cli/vyper_json/test_compile_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ def test_wrong_language():

def test_exc_handler_raises_syntax(input_json):
input_json["sources"]["badcode.vy"] = {"content": BAD_SYNTAX_CODE}
with pytest.raises(SyntaxException):
with pytest.raises(SyntaxException, match=r'contract "badcode\.vy:\d+"'):
compile_json(input_json)


Expand Down
8 changes: 8 additions & 0 deletions vyper/ast/natspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ class NatspecOutput:


def parse_natspec(annotated_vyper_module: vy_ast.Module) -> NatspecOutput:
try:
return _parse_natspec(annotated_vyper_module)
except NatSpecSyntaxException as e:
e.path = annotated_vyper_module.resolved_path
raise e


def _parse_natspec(annotated_vyper_module: vy_ast.Module) -> NatspecOutput:
"""
Parses NatSpec documentation from a contract.

Expand Down
19 changes: 18 additions & 1 deletion vyper/ast/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ def parse_to_ast_with_settings(
module_path: Optional[str] = None,
resolved_path: Optional[str] = None,
add_fn_node: Optional[str] = None,
) -> tuple[Settings, vy_ast.Module]:
try:
return _parse_to_ast_with_settings(
vyper_source, source_id, module_path, resolved_path, add_fn_node
)
except SyntaxException as e:
e.path = resolved_path
raise e


def _parse_to_ast_with_settings(
vyper_source: str,
source_id: int = 0,
module_path: Optional[str] = None,
resolved_path: Optional[str] = None,
add_fn_node: Optional[str] = None,
) -> tuple[Settings, vy_ast.Module]:
"""
Parses a Vyper source string and generates basic Vyper AST nodes.
Expand Down Expand Up @@ -60,7 +76,8 @@ def parse_to_ast_with_settings(
py_ast = python_ast.parse(pre_parser.reformatted_code)
except SyntaxError as e:
# TODO: Ensure 1-to-1 match of source_code:reformatted_code SyntaxErrors
raise SyntaxException(str(e), vyper_source, e.lineno, e.offset) from None
# SyntaxError offset is 1-based, not 0-based
Copy link
Member

@charles-cooper charles-cooper Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add docs to comment:

Suggested change
# SyntaxError offset is 1-based, not 0-based
# SyntaxError offset is 1-based, not 0-based
# https://docs.python.org/3/library/exceptions.html#SyntaxError.offset

raise SyntaxException(str(e.msg), vyper_source, e.lineno, e.offset - 1) from None

# Add dummy function node to ensure local variables are treated as `AnnAssign`
# instead of state variables (`VariableDecl`)
Expand Down
15 changes: 13 additions & 2 deletions vyper/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def __init__(self, message="Error Message not found.", *items, hint=None, prev_d
self.lineno = None
self.col_offset = None
self.annotations = None
self.path = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for clarity, let's rename this to self.resolved_path


if len(items) == 1 and isinstance(items[0], tuple) and isinstance(items[0][0], int):
# support older exceptions that don't annotate - remove this in the future!
Expand Down Expand Up @@ -127,13 +128,18 @@ def format_annotation(self, value):
module_node = node.module_node

# TODO: handle cases where module is None or vy_ast.Module
if module_node.get("path") not in (None, "<unknown>"):
node_msg = f'{node_msg}contract "{module_node.path}:{node.lineno}", '
if module_node.get("resolved_path") not in (None, "<unknown>"):
node_msg = self._format_contract_details(
node_msg, module_node.resolved_path, node.lineno
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we check .get("path") but we access .resolved_path - is that correct?

)

fn_node = node.get_ancestor(vy_ast.FunctionDef)
if fn_node:
node_msg = f'{node_msg}function "{fn_node.name}", '

elif self.path is not None:
node_msg = self._format_contract_details(node_msg, self.path, node.lineno)
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved

col_offset_str = "" if node.col_offset is None else str(node.col_offset)
node_msg = f"{node_msg}line {node.lineno}:{col_offset_str} \n{source_annotation}\n"

Expand All @@ -151,6 +157,11 @@ def _add_hint(self, msg):
return msg
return msg + f"\n (hint: {self.hint})"

def _format_contract_details(self, msg, path, lineno):
from vyper.utils import safe_relpath
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved

return f'{msg}contract "{safe_relpath(path)}:{lineno}", '

def __str__(self):
return self._add_hint(self._str_helper())

Expand Down
Loading