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[lang]: disallow absolute relative imports #4268

Merged
merged 36 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
84f9c70
fix absolute relative paths
sandbubbles Oct 1, 2024
0a69fca
lint
sandbubbles Oct 1, 2024
57f7abd
fix path from absolute to relative
sandbubbles Oct 3, 2024
0db6330
add test case for correct relative path
sandbubbles Oct 3, 2024
b1dee66
make relative paths work again
sandbubbles Oct 3, 2024
bc7d569
fix that relatives paths could break out of current directory
sandbubbles Oct 5, 2024
64ed8c3
add test for relative paths breaking out of current directory
sandbubbles Oct 5, 2024
7e3e20a
Merge branch 'master' into fix/relative-paths
sandbubbles Oct 5, 2024
6207819
merge master
sandbubbles Oct 15, 2024
0463a79
add _load_file with modifies search paths to a relative path or all a…
sandbubbles Oct 15, 2024
932e09a
add tests
sandbubbles Oct 15, 2024
67cb7d3
add resolved paths to import tests
sandbubbles Oct 15, 2024
052e895
lint
sandbubbles Oct 15, 2024
a4edbe9
revert back to old load_file
sandbubbles Oct 15, 2024
32eb376
fuse _load_import with _load_import_helper
sandbubbles Oct 15, 2024
e20b381
lint
sandbubbles Oct 15, 2024
a843639
fix typo and remove multiline expr
sandbubbles Oct 15, 2024
b98ea67
remove newly redundant methods
sandbubbles Oct 15, 2024
d75c292
lint
sandbubbles Oct 15, 2024
141ed2f
Merge branch 'master' into fix/relative-paths
charles-cooper Oct 16, 2024
f2ca3c8
Merge branch 'master' into fix/relative-paths
charles-cooper Oct 17, 2024
5ddc9b5
fix tests
sandbubbles Oct 21, 2024
6a7d5cc
remove redundancy and fixup
sandbubbles Oct 21, 2024
de79ba5
move function down
sandbubbles Oct 21, 2024
e4d6c62
remove newline
sandbubbles Oct 21, 2024
ec686f8
restore search paths after loading file
sandbubbles Oct 22, 2024
85e7eea
merge master
sandbubbles Nov 7, 2024
f0f325a
change "+=" to "append"
sandbubbles Dec 11, 2024
c91f13e
Merge branch 'master' into fix/relative-paths
sandbubbles Dec 11, 2024
a140226
clean up recursion
charles-cooper Dec 13, 2024
99f0af2
fix lint
charles-cooper Dec 13, 2024
3359eae
clean up test
charles-cooper Dec 13, 2024
f67dee8
minor clean up for some tests
charles-cooper Dec 13, 2024
69ce28e
some more test cleanup
charles-cooper Dec 13, 2024
0ad5e7f
clean up tests some more
sandbubbles Dec 14, 2024
be01034
Merge branch 'master' into fix/relative-paths
charles-cooper Dec 17, 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
26 changes: 23 additions & 3 deletions tests/functional/codegen/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,6 @@ def bar() -> uint256:
("import Foo as Foo", "Foo.vyi"),
("from a import Foo", "a/Foo.vyi"),
("from b.a import Foo", "b/a/Foo.vyi"),
("from .a import Foo", "./a/Foo.vyi"),
("from ..a import Foo", "../a/Foo.vyi"),
]


Expand All @@ -174,6 +172,26 @@ def test_extract_file_interface_imports(code, filename, make_input_bundle):
assert compile_code(code, input_bundle=input_bundle) is not None


VALID_RELATIVE_IMPORT_CODE = [
# import statement, import path without suffix
("from .a import Foo", "mock.vy"),
("from ..a import Foo", "b/mock.vy"),
]


# TODO CMC 2024-10-13: should probably be in syntax tests
@pytest.mark.parametrize("code,filename", VALID_RELATIVE_IMPORT_CODE)
def test_extract_file_interface_relative_imports(code, filename, make_input_bundle):
input_bundle = make_input_bundle({"a/Foo.vyi": "", filename: code})

assert (
compile_code(
code, resolved_path=(input_bundle.search_paths[0]) / filename, input_bundle=input_bundle
)
is not None
)


BAD_IMPORT_CODE = [
("import a as A\nimport a as A", DuplicateImport),
("import a as A\nimport a as a", DuplicateImport),
Expand All @@ -191,7 +209,9 @@ def test_extract_file_interface_imports_raises(
):
input_bundle = make_input_bundle({"a.vyi": "", "b/a.vyi": "", "c.vyi": ""})
with pytest.raises(exception_type):
compile_code(code, input_bundle=input_bundle)
compile_code(
code, resolved_path=input_bundle.search_paths[0] / "mock.vy", input_bundle=input_bundle
)


def test_external_call_to_interface(env, get_contract, make_input_bundle):
Expand Down
134 changes: 134 additions & 0 deletions tests/functional/syntax/test_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import pytest

from vyper import compiler
from vyper.exceptions import ModuleNotFound


def test_implicitly_relative_import_crashes(make_input_bundle):
top = """
import subdir0.lib0 as lib0
@external
def foo():
lib0.foo()
"""

lib0 = """
import subdir1.lib1 as lib1
def foo():
lib1.foo()
"""

lib1 = """
def foo():
pass
"""

input_bundle = make_input_bundle(
{"top.vy": top, "subdir0/lib0.vy": lib0, "subdir0/subdir1/lib1.vy": lib1}
)

with pytest.raises(ModuleNotFound):
compiler.compile_code(top, input_bundle=input_bundle)

lib0 = """
from subdir1 import lib1 as lib1
def foo():
lib1.foo()
"""

input_bundle = make_input_bundle(
{"top.vy": top, "subdir0/lib0.vy": lib0, "subdir0/subdir1/lib1.vy": lib1}
)

with pytest.raises(ModuleNotFound):
compiler.compile_code(top, input_bundle=input_bundle)


def test_relative_import_searches_only_current_path(make_input_bundle):
top = """
from subdir import b as b
@external
def foo():
b.foo()
"""

a = """
def foo():
pass
"""

b = """
from . import a as a
def foo():
a.foo()
"""

input_bundle = make_input_bundle({"top.vy": top, "a.vy": a, "subdir/b.vy": b})

with pytest.raises(ModuleNotFound):
compiler.compile_code(top, input_bundle=input_bundle)


def test_absolute_import_within_relative_import(make_input_bundle):
top = """
import subdir0.subdir1.c as c
@external
def foo():
c.foo()
"""
a = """
import subdir0.b as b
def foo():
b.foo()
"""
b = """
def foo():
pass
"""

c = """
from .. import a as a
def foo():
a.foo()
"""

input_bundle = make_input_bundle(
{"top.vy": top, "subdir0/a.vy": a, "subdir0/b.vy": b, "subdir0/subdir1/c.vy": c}
)
compiler.compile_code(top, input_bundle=input_bundle)


def test_absolute_path_passes(make_input_bundle):
top = """
import subdir0.lib0 as lib0
@external
def foo():
lib0.foo()
"""

lib0 = """
import subdir0.subdir1.lib1 as lib1
def foo():
lib1.foo()
"""

lib1 = """
def foo():
pass
"""

input_bundle = make_input_bundle(
{"top.vy": top, "subdir0/lib0.vy": lib0, "subdir0/subdir1/lib1.vy": lib1}
)
compiler.compile_code(top, input_bundle=input_bundle)

lib0 = """
from .subdir1 import lib1 as lib1
def foo():
lib1.foo()
"""

input_bundle = make_input_bundle(
{"top.vy": top, "subdir0/lib0.vy": lib0, "subdir0/subdir1/lib1.vy": lib1}
)
compiler.compile_code(top, input_bundle=input_bundle)
5 changes: 3 additions & 2 deletions tests/unit/cli/vyper_json/test_compile_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ def get(filename, contractname):
def test_relative_import_paths(input_json):
input_json["sources"]["contracts/potato/baz/baz.vy"] = {"content": "from ... import foo"}
input_json["sources"]["contracts/potato/baz/potato.vy"] = {"content": "from . import baz"}
input_json["sources"]["contracts/potato/footato.vy"] = {"content": "from baz import baz"}
input_json["sources"]["contracts/potato/footato.vy"] = {"content": "from .baz import baz"}
compile_from_input_dict(input_json)


Expand All @@ -319,7 +319,8 @@ def test_compile_json_with_abi_top(make_input_bundle):
from . import stream
"""
input_bundle = make_input_bundle({"stream.json": stream, "code.vy": code})
vyper.compiler.compile_code(code, input_bundle=input_bundle)
file_input = input_bundle.load_file("code.vy")
vyper.compiler.compile_from_file_input(file_input, input_bundle=input_bundle)


def test_compile_json_with_experimental_codegen():
Expand Down
14 changes: 5 additions & 9 deletions vyper/compiler/input_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,6 @@ def load_file(self, path: PathLike | str) -> CompilerInput:

return res

def add_search_path(self, path: PathLike) -> None:
self.search_paths.append(path)

# temporarily add something to the search path (within the
# scope of the context manager) with highest precedence.
# if `path` is None, do nothing
Expand All @@ -155,16 +152,15 @@ def search_path(self, path: Optional[PathLike]) -> Iterator[None]:
finally:
self.search_paths.pop()

# temporarily modify the top of the search path (within the
# scope of the context manager) with highest precedence to something else
# temporarily set search paths to a given list
@contextlib.contextmanager
def poke_search_path(self, path: PathLike) -> Iterator[None]:
tmp = self.search_paths[-1]
self.search_paths[-1] = path
def temporary_search_paths(self, new_paths: list[PathLike]) -> Iterator[None]:
original_paths = self.search_paths
self.search_paths = new_paths
try:
yield
finally:
self.search_paths[-1] = tmp
self.search_paths = original_paths


# regular input. takes a search path(s), and `load_file()` will search all
Expand Down
31 changes: 19 additions & 12 deletions vyper/semantics/analysis/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def __init__(self, input_bundle: InputBundle, graph: _ImportGraph):

self.integrity_sum = None

# should be all system paths + topmost module path
self.absolute_search_paths = input_bundle.search_paths.copy()

def resolve_imports(self, module_ast: vy_ast.Module):
self._resolve_imports_r(module_ast)
self.integrity_sum = self._calculate_integrity_sum_r(module_ast)
Expand Down Expand Up @@ -151,15 +154,7 @@ def _add_import(

# load an InterfaceT or ModuleInfo from an import.
# raises FileNotFoundError
def _load_import(self, node: vy_ast.VyperNode, level: int, module_str: str, alias: str) -> Any:
# the directory this (currently being analyzed) module is in
ast = self.graph.current_module
self_search_path = Path(ast.resolved_path).parent

with self.input_bundle.poke_search_path(self_search_path):
return self._load_import_helper(node, level, module_str, alias)

def _load_import_helper(
def _load_import(
self, node: vy_ast.VyperNode, level: int, module_str: str, alias: str
) -> tuple[CompilerInput, Any]:
if _is_builtin(module_str):
Expand All @@ -177,7 +172,7 @@ def _load_import_helper(

try:
path_vy = path.with_suffix(".vy")
file = self.input_bundle.load_file(path_vy)
file = self._load_file(path_vy, level)
assert isinstance(file, FileInput) # mypy hint

module_ast = self._ast_from_file(file)
Expand All @@ -191,7 +186,7 @@ def _load_import_helper(
err = e

try:
file = self.input_bundle.load_file(path.with_suffix(".vyi"))
file = self._load_file(path.with_suffix(".vyi"), level)
assert isinstance(file, FileInput) # mypy hint
module_ast = self._ast_from_file(file)
self.resolve_imports(module_ast)
Expand All @@ -205,7 +200,7 @@ def _load_import_helper(
pass

try:
file = self.input_bundle.load_file(path.with_suffix(".json"))
file = self._load_file(path.with_suffix(".json"), level)
assert isinstance(file, ABIInput) # mypy hint
return file, file.abi
except FileNotFoundError:
Expand All @@ -219,6 +214,18 @@ def _load_import_helper(
search_paths = self.input_bundle.search_paths.copy() # noqa: F841
raise ModuleNotFound(module_str, hint=hint) from err

def _load_file(self, path: PathLike, level: int) -> CompilerInput:
ast = self.graph.current_module

search_paths: list[PathLike] # help mypy
if level != 0: # relative import
search_paths = [Path(ast.resolved_path).parent]
else:
search_paths = self.absolute_search_paths

with self.input_bundle.temporary_search_paths(search_paths):
return self.input_bundle.load_file(path)

def _ast_from_file(self, file: FileInput) -> vy_ast.Module:
# cache ast if we have seen it before.
# this gives us the additional property of object equality on
Expand Down
Loading