Repository: vyperlang/vyper

Issue:
VersionException error is not helpful with imports
### Version Information

* vyper Version (output of `vyper --version`): 91ef0dd130de91081f044541c87f20d901d87d89

### What's your issue about?
compiling a contract which imports a module with an unexpected version results in
```
vyper.exceptions.VersionException: line 1:0 Version specification "~=0.3.10" is not compatible with compiler version "0.4.0b6"
```

### How can it be fixed?

raise error with source info


Discussion:

Resolving patch:
diff --git a/vyper/ast/pre_parser.py b/vyper/ast/pre_parser.py
--- a/vyper/ast/pre_parser.py
+++ b/vyper/ast/pre_parser.py
@@ -15,14 +15,14 @@
 from vyper.typing import ModificationOffsets, ParserPosition
 
 
-def validate_version_pragma(version_str: str, start: ParserPosition) -> None:
+def validate_version_pragma(version_str: str, full_source_code: str, start: ParserPosition) -> None:
     """
     Validates a version pragma directive against the current compiler version.
     """
     from vyper import __version__
 
     if len(version_str) == 0:
-        raise VersionException("Version specification cannot be empty", start)
+        raise VersionException("Version specification cannot be empty", full_source_code, *start)
 
     # X.Y.Z or vX.Y.Z => ==X.Y.Z, ==vX.Y.Z
     if re.match("[v0-9]", version_str):
@@ -34,14 +34,17 @@ def validate_version_pragma(version_str: str, start: ParserPosition) -> None:
         spec = SpecifierSet(version_str)
     except InvalidSpecifier:
         raise VersionException(
-            f'Version specification "{version_str}" is not a valid PEP440 specifier', start
+            f'Version specification "{version_str}" is not a valid PEP440 specifier',
+            full_source_code,
+            *start,
         )
 
     if not spec.contains(__version__, prereleases=True):
         raise VersionException(
             f'Version specification "{version_str}" is not compatible '
             f'with compiler version "{__version__}"',
-            start,
+            full_source_code,
+            *start,
         )
 
 
@@ -176,7 +179,7 @@ def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, dict, str]:
                     if settings.compiler_version is not None:
                         raise StructureException("compiler version specified twice!", start)
                     compiler_version = contents.removeprefix("@version ").strip()
-                    validate_version_pragma(compiler_version, start)
+                    validate_version_pragma(compiler_version, code, start)
                     settings.compiler_version = compiler_version
 
                 if contents.startswith("pragma "):
@@ -185,7 +188,7 @@ def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, dict, str]:
                         if settings.compiler_version is not None:
                             raise StructureException("pragma version specified twice!", start)
                         compiler_version = pragma.removeprefix("version ").strip()
-                        validate_version_pragma(compiler_version, start)
+                        validate_version_pragma(compiler_version, code, start)
                         settings.compiler_version = compiler_version
 
                     elif pragma.startswith("optimize "):
diff --git a/vyper/exceptions.py b/vyper/exceptions.py
--- a/vyper/exceptions.py
+++ b/vyper/exceptions.py
@@ -204,7 +204,7 @@ class InstantiationException(StructureException):
     """Variable or expression cannot be instantiated"""
 
 
-class VersionException(VyperException):
+class VersionException(SyntaxException):
     """Version string is malformed or incompatible with this compiler version."""
 
 


Test patch:
diff --git a/tests/unit/ast/test_pre_parser.py b/tests/unit/ast/test_pre_parser.py
--- a/tests/unit/ast/test_pre_parser.py
+++ b/tests/unit/ast/test_pre_parser.py
@@ -1,5 +1,6 @@
 import pytest
 
+from vyper import compile_code
 from vyper.ast.pre_parser import pre_parse, validate_version_pragma
 from vyper.compiler.phases import CompilerData
 from vyper.compiler.settings import OptimizationLevel, Settings
@@ -45,14 +46,14 @@ def set_version(version):
 @pytest.mark.parametrize("file_version", valid_versions)
 def test_valid_version_pragma(file_version, mock_version):
     mock_version(COMPILER_VERSION)
-    validate_version_pragma(f"{file_version}", (SRC_LINE))
+    validate_version_pragma(f"{file_version}", file_version, (SRC_LINE))
 
 
 @pytest.mark.parametrize("file_version", invalid_versions)
 def test_invalid_version_pragma(file_version, mock_version):
     mock_version(COMPILER_VERSION)
     with pytest.raises(VersionException):
-        validate_version_pragma(f"{file_version}", (SRC_LINE))
+        validate_version_pragma(f"{file_version}", file_version, (SRC_LINE))
 
 
 prerelease_valid_versions = [
@@ -82,14 +83,14 @@ def test_invalid_version_pragma(file_version, mock_version):
 @pytest.mark.parametrize("file_version", prerelease_valid_versions)
 def test_prerelease_valid_version_pragma(file_version, mock_version):
     mock_version(PRERELEASE_COMPILER_VERSION)
-    validate_version_pragma(file_version, (SRC_LINE))
+    validate_version_pragma(file_version, file_version, (SRC_LINE))
 
 
 @pytest.mark.parametrize("file_version", prerelease_invalid_versions)
 def test_prerelease_invalid_version_pragma(file_version, mock_version):
     mock_version(PRERELEASE_COMPILER_VERSION)
     with pytest.raises(VersionException):
-        validate_version_pragma(file_version, (SRC_LINE))
+        validate_version_pragma(file_version, file_version, (SRC_LINE))
 
 
 pragma_examples = [
@@ -224,3 +225,32 @@ def test_parse_pragmas(code, pre_parse_settings, compiler_data_settings, mock_ve
 def test_invalid_pragma(code):
     with pytest.raises(StructureException):
         pre_parse(code)
+
+
+def test_version_exception_in_import(make_input_bundle):
+    lib_version = "~=0.3.10"
+    lib = f"""
+#pragma version {lib_version}
+
+@external
+def foo():
+    pass
+    """
+
+    code = """
+import lib
+
+uses: lib
+
+@external
+def bar():
+    pass
+    """
+    input_bundle = make_input_bundle({"lib.vy": lib})
+
+    with pytest.raises(VersionException) as excinfo:
+        compile_code(code, input_bundle=input_bundle)
+    annotation = excinfo.value.annotations[0]
+    assert annotation.lineno == 2
+    assert annotation.col_offset == 0
+    assert annotation.full_source_code == lib